ConcurrentHashMap一定執行緒安全?

深夜裡的程式猿發表於2019-04-19

前言

老王為何半夜慘叫?幾行程式碼為何導致伺服器爆炸?說好的執行緒安全為何還是出問題?讓我們一起收看今天的《走進IT》

正文

CurrentHashMap出現背景

說到ConcurrentHashMap的出現背景,還得從HashMap說起。

老王是某公司的苦逼Java開發,在網際網路行業中,業務總是迭代得非常快。體現在程式碼中的話,就是v1.0的模組是單執行緒執行的,這時候使用HashMap是一個不錯的選擇。然而到了v1.5的版本,為了效能考慮,老王覺得把這段程式碼改成多執行緒會更有效率,那麼說改就改,然後就愉快的釋出上線了。

直到某天晚上,突然收到線上警報,伺服器CPU佔用100%。這時候驚醒起來一頓排查(百度,谷歌),結果發現原來是HashMap 在併發的環境下進行rehash的時候會造成連結串列的閉環,因此在進行get()操作的時候導致了CPU佔用100%。喔,原來HashMap不是執行緒安全的類,在當前的業務場景中會有問題。那麼你這時候又想到了Hashtable,沒錯,這是個執行緒安全的類,那我先用這個類替換不就行了,一頓commit,push,部署上去了,觀察了一段時間,完美~再也沒出現過類似的問題了。

但是好日子過的並不長久,運營的同事又找上門了,老王啊,XX功能怎麼慢了這麼多啊?這時候老王就納悶了,我沒改程式碼啊?不就上次替換了一個Hashtable,難道這裡會有效率的問題?然後又是一頓排查(百度、谷歌),我去,果不其然,原來它執行緒安全的原因是因為在方法上都加了synchronized,導致我們全部操作都序列化了,難怪這麼慢。

經過了2次掉陷阱的經驗,這次的老王已經是非常謹慎的去尋求更好的解決方案了,這時他找到ConcurrentHashMap,而且為了避免再次掉坑他也去提前瞭解了實現原理,原來這個類是使用了Segment分段鎖,每一個Segment都有自己的鎖,這樣衝突的的範圍就變小了,效率也能提高不少。經過調研發現確實不錯,於是他就放心的把Hashtable給替換掉了,從此運營再也沒來吐槽了,老王又過上了幸福的日子。

經過一段時間緊張的業務開發,此時的專案已經去到了v2.0,之前的ConcurrentHashMap相關的程式碼已經被改的面目全非,邏輯也複雜了很多,但專案還是按時順利的上線了。在專案在執行了一段時間以後,居然再次出現執行緒安全的問題,其根源竟然是ConcurrentHashMap,老王叕陷入了沉思...

為何會出問題?

拋開復雜的例子,我們用一個多執行緒併發獲取map中的值並加1,看看最後輸出的數字如何

public class CHMDemo {
    public static void main(String[] args) throws InterruptedException {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<String,Integer>();
        map.put("key", 1);
        ExecutorService executorService = Executors.newFixedThreadPool(100);
        for (int i = 0; i < 1000; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    int key = map.get("key") + 1; //step 1
                    map.put("key", key);//step 2
                }
            });
        }
        Thread.sleep(3000); //模擬等待執行結束
        System.out.println("------" + map.get("key") + "------");
        executorService.shutdown();
    }
}
複製程式碼

此時我們看看多次執行輸出的結果

------790------
------825------
------875------
複製程式碼

通過觀察輸出結果可以發現,這段使用ConcurrentHashMap的程式碼,產生了執行緒安全的問題。我們來分析一下為什麼會發生這種情況。在step1跟step2中,都只是呼叫ConcurrentHashMap的方法,各自都是原子操作,是執行緒安全的。但是他們組合在一起的時候就會有問題了,A執行緒在進入方法後,通過map.get("key")拿到key的值,剛把這個值讀取出來還沒有加1的時候,執行緒B也進來了,那麼這導致執行緒A和執行緒B拿到的key是一樣的。不僅僅是在

ConcurrentHashMap,在其他的執行緒安全的容器比如Vector之類的也會出現如此情況,所以在使用這些容器的時候還是不能大意。

如何解決?

1、可以用synchronized

synchronized(this){
    //step1
    //step2
}

複製程式碼

但是用這種方法的話,我們要考慮一下效率的問題,會不會對當前的業務影響很大?

2、用原子類

public class CHMDemo {
    public static void main(String[] args) throws InterruptedException {
        ConcurrentHashMap<String, AtomicInteger> map = new ConcurrentHashMap<String,AtomicInteger>();
        AtomicInteger integer = new AtomicInteger(1);
        map.put("key", integer);
        ExecutorService executorService = Executors.newFixedThreadPool(100);
        for (int i = 0; i < 1000; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    map.get("key").incrementAndGet();
                }
            });
        }
        Thread.sleep(3000); //模擬等待執行結束
        System.out.println("------" + map.get("key") + "------");
        executorService.shutdown();
    }
}
複製程式碼
------1001------
複製程式碼

此時的輸出結果就正確了,效率上也比第一種解決方案提高很多。

結語

人生處處是陷阱,寫程式碼也是如此,多思考,多留心。


推薦閱讀

大白話搞懂什麼是同步/非同步/阻塞/非阻塞
Java異常處理最佳實踐及陷阱防範
論JVM爆炸的幾種姿勢及自救方法
解放程式設計師雙手之Supervisor

有收穫的話,就點個贊吧

關注「深夜裡的程式猿」,分享最乾的乾貨

ConcurrentHashMap一定執行緒安全?

相關文章