使用ConcurrentHashMap必定線程安全?

前言

老王爲什麼半夜慘叫?幾行代碼爲什麼致使服務器爆炸?說好的線程安全爲什麼仍是出問題?讓咱們一塊兒收看今天的《走進IT》java

正文

CurrentHashMap出現背景

說到ConcurrentHashMap的出現背景,還得從HashMap提及。程序員

老王是某公司的苦逼Java開發,在互聯網行業中,業務老是迭代得很是快。體如今代碼中的話,就是v1.0的模塊是單線程執行的,這時候使用HashMap是一個不錯的選擇。然而到了v1.5的版本,爲了性能考慮,老王以爲把這段代碼改爲多線程會更有效率,那麼說改就改,而後就愉快的發佈上線了。安全

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

可是好日子過的並不長久,運營的同事又找上門了,老王啊,XX功能怎麼慢了這麼多啊?這時候老王就納悶了,我沒改代碼啊?不就上次替換了一個Hashtable,難道這裏會有效率的問題?而後又是一頓排查(百度、谷歌),我去,果不其然,原來它線程安全的緣由是由於在方法上都加了synchronized,致使咱們所有操做都串行化了,難怪這麼慢。服務器

通過了2次掉陷阱的經驗,此次的老王已是很是謹慎的去尋求更好的解決方案了,這時他找到ConcurrentHashMap,並且爲了不再次掉坑他也去提早了解了實現原理,原來這個類是使用了Segment分段鎖,每個Segment都有本身的鎖,這樣衝突的的範圍就變小了,效率也能提升很多。通過調研發現確實不錯,因而他就放心的把Hashtable給替換掉了,今後運營再也沒來吐槽了,老王又過上了幸福的日子。markdown

通過一段時間緊張的業務開發,此時的項目已經去到了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是同樣的。不只僅是在ide

ConcurrentHashMap,在其餘的線程安全的容器好比Vector之類的也會出現如此狀況,因此在使用這些容器的時候仍是不能大意。

如何解決?

一、能夠用synchronized

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

複製代碼

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

二、用原子類

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

有收穫的話,就點個贊吧

關注「深夜裏的程序猿」,分享最乾的乾貨

相關文章
相關標籤/搜索