最近做的項目中遇到一個問題:明明用了ConcurrentHashMap,可是始終線程不安全
除去項目中的業務邏輯,簡化後的代碼如下:
public class Test40 { public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 10; i++) { System.out.println(test()); } } private static int test() throws InterruptedException { ConcurrentHashMapmap = new ConcurrentHashMap (); ExecutorService pool = Executors.newCachedThreadPool(); for (int i = 0; i < 8; i++) { pool.execute(new MyTask(map)); } pool.shutdown(); pool.awaitTermination(1, TimeUnit.DAYS); return map.get(MyTask.KEY); } } class MyTask implements Runnable { public static final String KEY = "key"; private ConcurrentHashMap map; public MyTask(ConcurrentHashMap map) { this.map = map; } @Override public void run() { for (int i = 0; i < 100; i++) { this.addup(); } } private void addup() { if (!map.containsKey(KEY)) { map.put(KEY, 1); } else { map.put(KEY, map.get(KEY) + 1); } } }
查了一些資料後發現,原來ConcurrentHashMap的線程安全指的是,它的每個方法單獨調用(即原子操作)都是線程安全的,但是代碼總體的互斥性並不受控制。以上面的代碼為例,最後一行中的:
map.put(KEY, map.get(KEY) + 1);
其中第1和第3步,單獨來說都是線程安全的,由ConcurrentHashMap保證。但是由於在上面的代碼中,map本身是一個共享變量。當線程A執行map.get的時候,其它線程可能正在執行map.put,這樣一來當線程A執行到map.put的時候,線程A的值就已經是髒數據了,然後髒數據覆蓋了真值,導致線程不安全
簡單地說,ConcurrentHashMap的get方法獲取到的是此時的真值,但它並不保證當你調用put方法的時候,當時獲取到的值仍然是真值
為了使上面的代碼變得線程安全,我引入了synchronized關鍵字來修飾目標方法,如下:
public class Test40 { public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 10; i++) { System.out.println(test()); } } private static int test() throws InterruptedException { ConcurrentHashMapmap = new ConcurrentHashMap (); ExecutorService pool = Executors.newCachedThreadPool(); for (int i = 0; i < 8; i++) { pool.execute(new MyTask(map)); } pool.shutdown(); pool.awaitTermination(1, TimeUnit.DAYS); return map.get(MyTask.KEY); } } class MyTask implements Runnable { public static final String KEY = "key"; private ConcurrentHashMap map; public MyTask(ConcurrentHashMap map) { this.map = map; } @Override public void run() { for (int i = 0; i < 100; i++) { this.addup(); } } private synchronized void addup() { // 用關鍵字synchronized修飾addup方法 if (!map.containsKey(KEY)) { map.put(KEY, 1); } else { map.put(KEY, map.get(KEY) + 1); } } }
查閱了synchronized的資料後,原來,不管synchronized是用來修飾方法,還是修飾代碼塊,其本質都是鎖定某一個對象。修飾方法時,鎖上的是調用這個方法的對象,即this;修飾代碼塊時,鎖上的是括號裡的那個對象
在上面的代碼中,很明顯就是鎖定的MyTask對象本身。但是由於在每一個線程中,MyTask對象都是獨立的,這就導致實際上每個線程都對自己的MyTask進行鎖定,而並不會干涉其它線程的MyTask對象。換言之,上鎖壓根沒有意義
理解到這點之後,對上面的代碼又做了一次修改:
public class Test40 { public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 10; i++) { System.out.println(test()); } } private static int test() throws InterruptedException { ConcurrentHashMapmap = new ConcurrentHashMap (); ExecutorService pool = Executors.newCachedThreadPool(); for (int i = 0; i < 8; i++) { pool.execute(new MyTask(map)); } pool.shutdown(); pool.awaitTermination(1, TimeUnit.DAYS); return map.get(MyTask.KEY); } } class MyTask implements Runnable { public static final String KEY = "key"; private ConcurrentHashMap map; public MyTask(ConcurrentHashMap map) { this.map = map; } @Override public void run() { for (int i = 0; i < 100; i++) { synchronized (map) { // 對共享對象map上鎖 this.addup(); } } } private void addup() { if (!map.containsKey(KEY)) { map.put(KEY, 1); } else { map.put(KEY, map.get(KEY) + 1); } } }
修改後,ConcurrentHashMap的作用就不大了,可以直接將代碼中的map換成普通的HashMap,以減少由ConcurrentHashMap帶來的鎖開銷
最後特別補充的是,synchronized關鍵字判斷對象是否是它屬於鎖定的對象,本質上是通過 == 運算符來判斷的。換句話說,上面的代碼中,可以采用任何一個常量,或者每個線程都共享的變量,或者MyTask類的靜態變量,來代替map。只要該變量與synchronized鎖定的目標變量相同(==),就可以使synchronized生效
綜上,代碼最終可以修改為:
public class Test40 { public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 100; i++) { System.out.println(test()); } } private static int test() throws InterruptedException { Mapmap = new HashMap (); ExecutorService pool = Executors.newCachedThreadPool(); for (int i = 0; i < 8; i++) { pool.execute(new MyTask(map)); } pool.shutdown(); pool.awaitTermination(1, TimeUnit.DAYS); return map.get(MyTask.KEY); } } class MyTask implements Runnable { public static Object lock = new Object(); public static final String KEY = "key"; private Map map; public MyTask(Map map) { this.map = map; } @Override public void run() { for (int i = 0; i < 100; i++) { synchronized (lock) { this.addup(); } } } private void addup() { if (!map.containsKey(KEY)) { map.put(KEY, 1); } else { map.put(KEY, map.get(KEY) + 1); } } }