ConcurrentHashMap 1.8為什么要使用CAS+Synchronized取代Segment+ReentrantLock


原文:https://www.cnblogs.com/yangfeiORfeiyang/p/9694383.html

 

大家應該都知道ConcurrentHashMap在1.8的時候有了很大的改動,當然,我這里要說的改動不是指鏈表長度大於8就轉為紅黑樹這種常識,我要說的是ConcurrentHashMap在1.8為什么用CAS+Synchronized取代Segment+ReentrantLock了

首先,我假設你對CAS,Synchronized,ReentrantLock這些知識很了解,並且知道AQS,自旋鎖,偏向鎖,輕量級鎖,重量級鎖這些知識,也知道Synchronized和ReentrantLock在喚醒被掛起線程競爭的時候有什么區別

首先我們說下1.8以前的ConcurrentHashMap是怎么保證線程並發的,首先在初始化ConcurrentHashMap的時候,會初始化一個Segment數組,容量為16,而每個Segment呢,都繼承了ReentrantLock類,也就是說每個Segment類本身就是一個鎖,之后Segment內部又有一個table數組,而每個table數組里的索引數據呢,又對應着一個Node鏈表.

那么這樣的好處是什么呢?我先從老版本的添加流程說起吧,由於電腦里沒有JDK1.7及以下的版本我沒法給你看代碼,所以使用文字描述的方式,首先,當我們使用put方法的時候,是對我們的key進行hash拿到一個整型,然后將整型對16取模,拿到對應的Segment,之后調用Segment的put方法,然后上鎖,請注意,這里lock()的時候其實是this.lock(),也就是說,每個Segment的鎖是分開的

其中一個上鎖不會影響另一個,此時也就代表了我可以有十六個線程進來,而ReentrantLock上鎖的時候如果只有一個線程進來,是不會有線程掛起的操作的,也就是說只需要在AQS里使用CAS改變一個state的值為1,此時就能對代碼進行操作,這樣一來,我們等於將並發量/16了.

好,說完了老版本的ConcurrentHashMap,我們再說說新版本的,請看下面的圖:

請注意Synchronized上鎖的對象,請記住,Synchronized是靠對象的對象頭和此對象對應的monitor來保證上鎖的,也就是對象頭里的重量級鎖標志指向了monitor,而monitor呢,內部則保存了一個當前線程,也就是搶到了鎖的線程.

那么這里的這個f是什么呢?它是Node鏈表里的每一個Node,也就是說,Synchronized是將每一個Node對象作為了一個鎖,這樣做的好處是什么呢?將鎖細化了,也就是說,除非兩個線程同時操作一個Node,注意,是一個Node而不是一個Node鏈表哦,那么才會爭搶同一把鎖.

如果使用ReentrantLock其實也可以將鎖細化成這樣的,只要讓Node類繼承ReentrantLock就行了,這樣的話調用f.lock()就能做到和Synchronized(f)同樣的效果,但為什么不這樣做呢?

請大家試想一下,鎖已經被細化到這種程度了,那么出現並發爭搶的可能性還高嗎?還有就是,哪怕出現爭搶了,只要線程可以在30到50次自旋里拿到鎖,那么Synchronized就不會升級為重量級鎖,而等待的線程也就不用被掛起,我們也就少了掛起和喚醒這個上下文切換的過程開銷.

但如果是ReentrantLock呢?它則只有在線程沒有搶到鎖,然后新建Node節點后再嘗試一次而已,不會自旋,而是直接被掛起,這樣一來,我們就很容易會多出線程上下文開銷的代價.當然,你也可以使用tryLock(),但是這樣又出現了一個問題,你怎么知道tryLock的時間呢?在時間范圍里還好,假如超過了呢?

所以,在鎖被細化到如此程度上,使用Synchronized是最好的選擇了.這里再補充一句,Synchronized和ReentrantLock他們的開銷差距是在釋放鎖時喚醒線程的數量,Synchronized是喚醒鎖池里所有的線程+剛好來訪問的線程,而ReentrantLock則是當前線程后進來的第一個線程+剛好來訪問的線程.

如果是線程並發量不大的情況下,那么Synchronized因為自旋鎖,偏向鎖,輕量級鎖的原因,不用將等待線程掛起,偏向鎖甚至不用自旋,所以在這種情況下要比ReentrantLock高效


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM