程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> ConcurrentHashMap、synchronized與線程安全

ConcurrentHashMap、synchronized與線程安全

編輯:C++入門知識

ConcurrentHashMap、synchronized與線程安全


最近做的項目中遇到一個問題:明明用了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 {
        ConcurrentHashMap map = 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);
        }    
    }
}

測試代碼跑了10次,每次都不是800。這就很讓人疑惑了,難道ConcurrentHashMap的線程安全性失效了?

查了一些資料後發現,原來ConcurrentHashMap的線程安全指的是,它的每個方法單獨調用(即原子操作)都是線程安全的,但是代碼總體的互斥性並不受控制。以上面的代碼為例,最後一行中的:

map.put(KEY, map.get(KEY) + 1);

實際上並不是原子操作,它包含了三步:

map.get加1map.put

其中第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 {
        ConcurrentHashMap map = 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的資料後,原來,不管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 {
        ConcurrentHashMap map = 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);
        }
    }
    
}

此時在調用addup時直接鎖定map,由於map是被所有線程共享的,因而達到了讓所有線程互斥的目的,線程安全達成。

修改後,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 {
        Map map = 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);
        }
    }
    
}


  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved