本文來源於實際專案。
專案需求:某段邏輯需要過濾註冊使用者,而每時每刻都可能會有新的註冊使用者加入進來。註冊使用者的存在與否是通過查詢資料庫表中是否存在記錄判斷的。由於不希望頻繁的讀資料庫表,所以考慮定時從資料庫載入一份使用者列表到記憶體裡,這樣可以減少讀庫的次數並且可以提高查詢的效率。
過濾使用者邏輯程式碼簡單抽象成下面的測試程式碼。
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());
}
}
}
這段程式碼希望完成的事情如下:
- 使用全域性變數userIDMap,它是一個HashMap型別的變數。希望通過呼叫map.containsKey(String)方法判斷使用者是否為合法使用者。
- 主執行緒呼叫start()方法,將所有使用者列表從資料庫里載入到記憶體裡,並賦值給userIDMap。
- 用執行緒池建立10個UserFilter任務,UserFilter任務只做一件事情,即通過呼叫isValidUser(String)方法檢查使用者是否是合法使用者
- isValidUser方法實現時會呼叫userIDMap.containsKey(String)方法,如果返回true,即userIDMap裡存在相應的使用者,表示使用者合法。
- 在呼叫userIDMap.containsKey(String)方法之前需要保證userIDMap裡已經存放了資料庫中的所有使用者列表。為此考慮使用java.lang.Object.wait()和java.lang.Object.notifyAll()方法。
- 在isValidUser方法開始處判斷userIDMap中是否有值(if userIDMap.size()==0),如果userIDMap還是空的,則呼叫wait方法,讓執行緒等待。
- 在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?我最初想到了下面兩種方式:
- 直接使用賦值語句。會使得左側變數引用新的物件。
- 使用 Map.clear() 方法清除 map中的資料,然後 Map.putAll(Map)
很遺憾一開始使用了“簡潔”一點的賦值操作,導致花了很長時間排查bug。
最後對程式碼做如下修改,問題解決:
public static void start() {
// 修改後的userIDMap“賦值”方式
userIDMap.clear();
userIDMap.putAll(userIDMap);
// userIDMap = dao.queryAllUsers();
synchronized(userIDMap) {
// 已經賦值,喚醒所有處於等待狀態的執行緒
userIDMap.notifyAll();
}
}