明明用了ConcurrentHashMap,可是始終線程不安全,
下面我們來看代碼:
1 public class Test40 { 2 3 public static void main(String[] args) throws InterruptedException { 4 for (int i = 0; i < 10; i++) { 5 System.out.println(test()); 6 } 7 } 8 9 private static int test() throws InterruptedException { 10 ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<String, Integer>(); 11 ExecutorService pool = Executors.newCachedThreadPool(); 12 for (int i = 0; i < 8; i++) { 13 pool.execute(new MyTask(map)); 14 } 15 pool.shutdown(); 16 pool.awaitTermination(1, TimeUnit.DAYS); 17 18 return map.get(MyTask.KEY); 19 } 20 } 21 22 class MyTask implements Runnable { 23 24 public static final String KEY = "key"; 25 26 private ConcurrentHashMap<String, Integer> map; 27 28 public MyTask(ConcurrentHashMap<String, Integer> map) { 29 this.map = map; 30 } 31 32 @Override 33 public void run() { 34 for (int i = 0; i < 100; i++) { 35 this.addup(); 36 } 37 } 38 39 private void addup() { 40 if (!map.containsKey(KEY)) { 41 map.put(KEY, 1); 42 } else { 43 map.put(KEY, map.get(KEY) + 1); 44 } 45 } 46 }
測試代碼跑了10次,每次都不是800。這就很讓人疑惑了,難道ConcurrentHashMap的線程安全性失效了?
查了一些資料后發現,原來ConcurrentHashMap的線程安全指的是,它的每個方法單獨調用(即原子操作)都是線程安全的,但是代碼總體的互斥性並不受控制。以上面的代碼為例,最后一行中的:
1 map.put(KEY, map.get(KEY) + 1);
實際上並不是原子操作,它包含了三步:
- map.get
- 加1
- map.put
其中第1和第3步,單獨來說都是線程安全的,由ConcurrentHashMap保證。但是由於在上面的代碼中,map本身是一個共享變量。當線程A執行map.get的時候,其它線程可能正在執行map.put,這樣一來當線程A執行到map.put的時候,線程A的值就已經是臟數據了,然后臟數據覆蓋了真值,導致線程不安全
簡單地說,ConcurrentHashMap的get方法獲取到的是此時的真值,但它並不保證當你調用put方法的時候,當時獲取到的值仍然是真值
為了使上面的代碼變得線程安全,我引入了synchronized關鍵字來修飾目標方法,如下:
1 public class Test40 { 2 3 public static void main(String[] args) throws InterruptedException { 4 for (int i = 0; i < 10; i++) { 5 System.out.println(test()); 6 } 7 } 8 9 private static int test() throws InterruptedException { 10 ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<String, Integer>(); 11 ExecutorService pool = Executors.newCachedThreadPool(); 12 for (int i = 0; i < 8; i++) { 13 pool.execute(new MyTask(map)); 14 } 15 pool.shutdown(); 16 pool.awaitTermination(1, TimeUnit.DAYS); 17 18 return map.get(MyTask.KEY); 19 } 20 } 21 22 class MyTask implements Runnable { 23 24 public static final String KEY = "key"; 25 26 private ConcurrentHashMap<String, Integer> map; 27 28 public MyTask(ConcurrentHashMap<String, Integer> map) { 29 this.map = map; 30 } 31 32 @Override 33 public void run() { 34 for (int i = 0; i < 100; i++) { 35 this.addup(); 36 } 37 } 38 39 private synchronized void addup() { // 用關鍵字synchronized修飾addup方法 40 if (!map.containsKey(KEY)) { 41 map.put(KEY, 1); 42 } else { 43 map.put(KEY, map.get(KEY) + 1); 44 } 45 } 46 47 }
運行之后仍然是線程不安全的,難道synchronized也失效了?
查閱了synchronized的資料后,原來,不管synchronized是用來修飾方法,還是修飾代碼塊,其本質都是鎖定某一個對象。修飾方法時,鎖上的是調用這個方法的對象,即this;修飾代碼塊時,鎖上的是括號里的那個對象
在上面的代碼中,很明顯就是鎖定的MyTask對象本身。但是由於在每一個線程中,MyTask對象都是獨立的,這就導致實際上每個線程都對自己的MyTask進行鎖定,而並不會干涉其它線程的MyTask對象。換言之,上鎖壓根沒有意義
理解到這點之后,對上面的代碼又做了一次修改:
1 public class Test40 { 2 3 public static void main(String[] args) throws InterruptedException { 4 for (int i = 0; i < 10; i++) { 5 System.out.println(test()); 6 } 7 } 8 9 private static int test() throws InterruptedException { 10 ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<String, Integer>(); 11 ExecutorService pool = Executors.newCachedThreadPool(); 12 for (int i = 0; i < 8; i++) { 13 pool.execute(new MyTask(map)); 14 } 15 pool.shutdown(); 16 pool.awaitTermination(1, TimeUnit.DAYS); 17 18 return map.get(MyTask.KEY); 19 } 20 } 21 22 class MyTask implements Runnable { 23 24 public static final String KEY = "key"; 25 26 private ConcurrentHashMap<String, Integer> map; 27 28 public MyTask(ConcurrentHashMap<String, Integer> map) { 29 this.map = map; 30 } 31 32 @Override 33 public void run() { 34 for (int i = 0; i < 100; i++) { 35 synchronized (map) { // 對共享對象map上鎖 36 this.addup(); 37 } 38 } 39 } 40 41 private void addup() { 42 if (!map.containsKey(KEY)) { 43 map.put(KEY, 1); 44 } else { 45 map.put(KEY, map.get(KEY) + 1); 46 } 47 } 48 49 }
此時在調用addup時直接鎖定map,由於map是被所有線程共享的,因而達到了讓所有線程互斥的目的,線程安全達成。
修改后,ConcurrentHashMap的作用就不大了,可以直接將代碼中的map換成普通的HashMap,以減少由ConcurrentHashMap帶來的鎖開銷
最后特別補充的是,synchronized關鍵字判斷對象是否是它屬於鎖定的對象,本質上是通過 == 運算符來判斷的。換句話說,上面的代碼中,可以采用任何一個常量,或者每個線程都共享的變量,或者MyTask類的靜態變量,來代替map。只要該變量與synchronized鎖定的目標變量相同(==),就可以使synchronized生效
綜上,代碼最終可以修改為:
1 public class Test40 { 2 3 public static void main(String[] args) throws InterruptedException { 4 for (int i = 0; i < 100; i++) { 5 System.out.println(test()); 6 } 7 } 8 9 private static int test() throws InterruptedException { 10 Map<String, Integer> map = new HashMap<String, Integer>(); 11 ExecutorService pool = Executors.newCachedThreadPool(); 12 for (int i = 0; i < 8; i++) { 13 pool.execute(new MyTask(map)); 14 } 15 pool.shutdown(); 16 pool.awaitTermination(1, TimeUnit.DAYS); 17 18 return map.get(MyTask.KEY); 19 } 20 } 21 22 class MyTask implements Runnable { 23 24 public static Object lock = new Object(); 25 26 public static final String KEY = "key"; 27 28 private Map<String, Integer> map; 29 30 public MyTask(Map<String, Integer> map) { 31 this.map = map; 32 } 33 34 @Override 35 public void run() { 36 for (int i = 0; i < 100; i++) { 37 synchronized (lock) { 38 this.addup(); 39 } 40 } 41 } 42 43 private void addup() { 44 if (!map.containsKey(KEY)) { 45 map.put(KEY, 1); 46 } else { 47 map.put(KEY, map.get(KEY) + 1); 48 } 49 } 50 51 }