記錄一段 Object wait()、notifyAll() 方法不當使用的經歷

iteye_401發表於2013-04-02

本文來源於實際專案。

專案需求:某段邏輯需要過濾註冊使用者,而每時每刻都可能會有新的註冊使用者加入進來。註冊使用者的存在與否是通過查詢資料庫表中是否存在記錄判斷的。由於不希望頻繁的讀資料庫表,所以考慮定時從資料庫載入一份使用者列表到記憶體裡,這樣可以減少讀庫的次數並且可以提高查詢的效率。

 

過濾使用者邏輯程式碼簡單抽象成下面的測試程式碼。

package test.java;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class UserFilter implements Runnable {
	private static IUserDAO dao = ServiceFactory.getUserDao(); 
	public static Map<String, String> userIDMap = new HashMap<String, String>();
	
	public static void start() {
		// 將資料庫表中的使用者資料載入到記憶體(以HashMap存放)
		// 賦值userIDMap
		userIDMap = dao.queryAllUsers();
		synchronized(userIDMap) {
			// 已經賦值,喚醒所有處於等待狀態的執行緒
			userIDMap.notifyAll();
		}
	}
	
	public static boolean isValidUser(String key) {
		synchronized(userIDMap) {
			if (userIDMap.size() == 0) {
				try {
					// 等待主執行緒呼叫start()方法對userIDMap賦值 
					userIDMap.wait();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			return userIDMap.containsKey(key);
		}
	}
	
	@Override
	public void run() {
		if (isValidUser("test")) {
			System.out.println("this is a valid user.");
		}
	}
	
	public static void main(String[] args) {
		// 主執行緒呼叫start方法
		start();
		ExecutorService exec = Executors.newCachedThreadPool();
		// 使用執行緒池建立10個執行緒
		for (int i = 0; i < 10; i++) {
			exec.execute(new UserFilter());
		}
	}
}
 
這段程式碼希望完成的事情如下:
  1. 使用全域性變數userIDMap,它是一個HashMap型別的變數。希望通過呼叫map.containsKey(String)方法判斷使用者是否為合法使用者。
  2. 主執行緒呼叫start()方法,將所有使用者列表從資料庫里載入到記憶體裡,並賦值給userIDMap。
  3. 用執行緒池建立10個UserFilter任務,UserFilter任務只做一件事情,即通過呼叫isValidUser(String)方法檢查使用者是否是合法使用者
  4. isValidUser方法實現時會呼叫userIDMap.containsKey(String)方法,如果返回true,即userIDMap裡存在相應的使用者,表示使用者合法。
  5. 在呼叫userIDMap.containsKey(String)方法之前需要保證userIDMap裡已經存放了資料庫中的所有使用者列表。為此考慮使用java.lang.Object.wait()和java.lang.Object.notifyAll()方法。
  6. 在isValidUser方法開始處判斷userIDMap中是否有值(if userIDMap.size()==0),如果userIDMap還是空的,則呼叫wait方法,讓執行緒等待。
  7. 在start()方法中,將通過資料庫dao呼叫查詢介面返回的map賦值給userIDMap。之後呼叫notifyAll()方法喚醒所有等待的執行緒。

但是,測試發現,所有執行緒進入等待狀態後都不能被正常喚醒。能看出問題出在哪兒麼?

---------------------------------------------------------------------------------------------

在上面的程式碼中,我使用userIDMap作為呼叫wait()和notifyAll()方法的Object物件,並且呼叫放在了兩個synchronized(userIDMap) 塊中。

 

來看下wait()和notifyAll()方法的具體用法(下面是根據jdk中的方法註釋概括出來的)

在某個執行緒方法中對wait()和notifyAll()的呼叫必須指定一個Object物件,而且該執行緒必須擁有該Object物件的monitor。最簡單的獲取到物件monitor的辦法是,在物件上使用synchronized關鍵字。當呼叫wait()方法後,當前執行緒會釋放掉物件鎖,並進入sleep狀態。其他執行緒在呼叫notifyAll()方法時必須使用同一個Object物件,notifyAll()方法成功呼叫後,所有在同一Obejct物件上等待的執行緒被喚醒。

 

這裡有個很關鍵的點,即兩個方法在不同執行緒裡被呼叫時必須作用在同一個物件上。

 

然後再仔細看下上面程式碼中start()方法是怎麼寫的。

public static void start() {
		// 將資料庫表中的使用者資料載入到記憶體(以HashMap存放)
		// 賦值userIDMap
		userIDMap = dao.queryAllUsers();
		synchronized(userIDMap) {
			// 已經賦值,喚醒所有處於等待狀態的執行緒
			userIDMap.notifyAll();
		}
	}

 

是的,userIDMap被賦值了!導致下面的synchronized作用到另一個物件上面,即使該物件現在也叫userIDMap。這裡的本意是想將查詢得到的使用者列表放入全域性維護的userIDMap中,通過賦值雖然可以實現這個需求,但卻讓usreIDMap引用了一個全新的物件。從這個上下文看,為了保證userIDMap引用同一個物件,需要考慮其他的途徑。

 

那麼,如何將一個Map的值複製到另一個Map?我最初想到了下面兩種方式:

  1. 直接使用賦值語句。會使得左側變數引用新的物件。
  2. 使用 Map.clear() 方法清除 map中的資料,然後 Map.putAll(Map)

很遺憾一開始使用了“簡潔”一點的賦值操作,導致花了很長時間排查bug。

 

最後對程式碼做如下修改,問題解決:

public static void start() {
		// 修改後的userIDMap“賦值”方式
		userIDMap.clear();
		userIDMap.putAll(userIDMap);
		// userIDMap = dao.queryAllUsers();
		synchronized(userIDMap) {
			// 已經賦值,喚醒所有處於等待狀態的執行緒
			userIDMap.notifyAll();
		}
	}

 

 

 

相關文章