理解鎖的基本知識
1. 鎖的類型
鎖從宏觀上分類,分為悲觀鎖與樂觀鎖畏纲。
樂觀鎖
樂觀鎖是一種樂觀思想凉唐,即認(rèn)為讀多寫少,遇到并發(fā)寫的可能性低霍骄,每次去拿數(shù)據(jù)的時候都認(rèn)為別人不會修改台囱,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數(shù)據(jù)读整,采取在寫時先讀出當(dāng)前版本號簿训,然后加鎖操作(比較跟上一次的版本號,如果一樣則更新)米间,如果失敗則要重復(fù)讀-比較-寫的操作强品。
java 中的樂觀鎖基本都是通過 CAS 操作實現(xiàn)的,CAS 是一種更新的原子操作屈糊,比較當(dāng)前值跟傳入值是否一樣的榛,一樣則更新,否則失敗逻锐。悲觀鎖
悲觀鎖是就是悲觀思想夫晌,即認(rèn)為寫多雕薪,遇到并發(fā)寫的可能性高,每次去拿數(shù)據(jù)的時候都認(rèn)為別人會修改晓淀,所以每次在讀寫數(shù)據(jù)的時候都會上鎖所袁,這樣別人想讀寫這個數(shù)據(jù)就會 block 直到拿到鎖。java 中的悲觀鎖就是Synchronized
凶掰,AQS 框架下的鎖則是先嘗試 CAS 樂觀鎖去獲取鎖燥爷,獲取不到,才會轉(zhuǎn)換為悲觀鎖懦窘,如ReentrantLock
前翎。
2. Java 線程阻塞的代價
java 的線程是映射到操作系統(tǒng)原生線程之上的,如果要阻塞或喚醒一個線程就需要操作系統(tǒng)介入畅涂,需要在用戶態(tài)與核心態(tài)之間切換港华,這種切換會消耗大量的系統(tǒng)資源,操作系統(tǒng)的這種切換操作稱之為互斥(mutex)毅戈。
JDK1.6之前,Synchronized
會導(dǎo)致爭用不到鎖的線程進(jìn)入阻塞狀態(tài)愤惰,被稱為重量級鎖苇经,為了緩解上述性能問題,JVM 從1.6開始宦言,對Synchronized
關(guān)鍵字做了優(yōu)化扇单,引入了輕量鎖與偏向鎖,默認(rèn)啟用了自旋鎖奠旺,他們都屬于樂觀鎖蜘澜。
3. markword
HotSpot 虛擬機的對象頭包括兩部分信息
- markword:用于存儲對象自身的運行時數(shù)據(jù),如哈希碼(HashCode)响疚、GC分代年齡鄙信、鎖狀態(tài)標(biāo)志、線程持有的鎖忿晕、偏向線程ID装诡、偏向時間戳等,這部分?jǐn)?shù)據(jù)的長度在32位和64位的虛擬機(未開啟壓縮指針)中分別為32bit和64bit践盼,官方稱它為“MarkWord”鸦采。
- klass:對象指向它的類元數(shù)據(jù)的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例咕幻。
上圖中,我們提到了 java 的4種鎖吐限,他們分別是重量級鎖鲜侥、自旋鎖、輕量級鎖和偏向鎖诸典。不同的鎖有不同的特點描函,每種鎖只有在其特定的場景下,才會有出色的表現(xiàn)狐粱,java 中沒有哪種鎖能夠在所有情況下都能有出色的效率舀寓,引入這么多鎖的原因就是為了應(yīng)對不同的場景。
前面講到了重量級鎖是悲觀鎖的一種肌蜻,自旋鎖互墓、輕量級鎖與偏向鎖屬于樂觀鎖。下面我們具體來分析下他們的特效蒋搜;
自旋鎖
自旋鎖原理非常簡單篡撵,如果持有鎖的線程能在很短時間內(nèi)釋放鎖資源,那么那些等待競爭鎖的線程就不需要做內(nèi)核態(tài)和用戶態(tài)之間的切換進(jìn)入阻塞掛起狀態(tài)豆挽,它們只需要等一等(自旋)育谬,等持有鎖的線程釋放鎖后即可立即獲取鎖,這樣就避免用戶線程和內(nèi)核的切換的消耗帮哈。
但是線程自旋是需要消耗 cup 的膛檀,說白了就是讓 cup 在做無用功,如果一直獲取不到鎖娘侍,那線程也不能一直占用 cup 自旋做無用功咖刃,所以需要設(shè)定一個自旋等待的最大時間。
如果持有鎖的線程執(zhí)行的時間超過自旋等待的最大時間扔沒有釋放鎖憾筏,就會導(dǎo)致其它爭用鎖的線程在最大等待時間內(nèi)還是獲取不到鎖嚎杨,這時爭用線程會停止自旋進(jìn)入阻塞狀態(tài)。
JVM 對于自旋周期的選擇氧腰,jdk1.6前這個限度是一定的寫死的磕潮,在1.6后引入了適應(yīng)性自旋鎖,適應(yīng)性自旋鎖意味著自旋的時間不再是固定的了容贝,而是由前一次在同一個鎖上的自旋時間以及鎖的擁有者的狀態(tài)來決定自脯。線程如果自旋成功了,則下次自旋的次數(shù)會更多斤富,如果自旋失敗了膏潮,則自旋的次數(shù)就會減少。
重量級鎖 Synchronized
Synchronized 的作用相信大家都已經(jīng)非常熟悉了满力;
它可以把任意一個非NULL的對象當(dāng)作鎖:
- 作用于方法焕参,鎖住的是對象的實例(this);
- 當(dāng)作用于靜態(tài)方法時轻纪,鎖住的是 Class 實例,又因為 Class 的相關(guān)數(shù)據(jù)存儲在永久帶 PermGen(jdk1.8則是 metaspace)叠纷,永久帶是全局共享的刻帚,因此靜態(tài)方法鎖相當(dāng)于類的一個全局鎖,會鎖所有調(diào)用該方法的線程涩嚣;
- synchronized 作用于一個對象實例時崇众,鎖住的是所有以該對象為鎖的代碼塊。
偏向鎖
所謂的偏向航厚,就是偏心顷歌,即鎖會偏向于當(dāng)前已經(jīng)占有鎖的線程 。
假如大部分情況是沒有競爭的(某個同步塊大多數(shù)情況都不會出現(xiàn)多線程同時競爭鎖)幔睬,那么可以通過偏向來提高性能眯漩。即在無競爭時,之前獲得鎖的線程再次獲得鎖時麻顶,會判斷是否偏向鎖指向我赦抖,那么該線程將不用再次獲得鎖,直接就可以進(jìn)入同步塊辅肾;如果未指向當(dāng)前線程队萤,則當(dāng)前線程會采用 CAS 操作將 markword 中線程ID設(shè)置為當(dāng)前線程ID,如果 CAS 操作成功宛瞄,則獲取偏向鎖成功浮禾,去執(zhí)行同步代碼塊交胚,如果 CAS 操作失敗份汗,則表示有競爭,獲得偏向鎖的線程被掛起蝴簇,偏向鎖升級為輕量級鎖杯活。
偏向鎖的實現(xiàn)就是將對象頭 markword 的標(biāo)記設(shè)置為偏向,并將線程ID寫入對象頭 markword熬词,當(dāng)其他線程請求相同的鎖時旁钧,偏向模式結(jié)束。
JVM默認(rèn)啟用偏向鎖互拾,使用如下的JVM參數(shù)來設(shè)置偏向鎖:
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=4
BiasedLockingStartupDelay 表示系統(tǒng)啟動幾秒鐘后啟用偏向鎖歪今。默認(rèn)為4秒,原因在于颜矿,系統(tǒng)剛啟動時寄猩,一般數(shù)據(jù)競爭是比較激烈的,此時啟用偏向鎖會降低性能骑疆。
輕量級鎖
輕量級鎖(Lightweight Locking)本意是為了減少多線程進(jìn)入互斥(mutex)的幾率田篇,并不是要替代互斥替废。
它采用 CAS 嘗試將對象的 markword 更新為指向當(dāng)前線程的指針,在進(jìn)入互斥前泊柬,進(jìn)行補救椎镣。
輕量級鎖是由偏向鎖升級來的,輕量鎖存在的目的是盡可能不用動用操作系統(tǒng)層面的互斥兽赁,因為那個性能會比較差状答。因為 JVM 本身就是一個應(yīng)用,所以希望在應(yīng)用層面上就解決線程同步問題闸氮。
總結(jié)一下就是輕量級鎖是一種快速的鎖定方法剪况,在進(jìn)入互斥之前,使用 CAS 操作來嘗試加鎖蒲跨,盡量不要用操作系統(tǒng)層面的互斥译断,提高了性能。
偏向鎖升級為輕量鎖的步驟為:
- 虛擬機首先將在當(dāng)前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間或悲,用于存儲鎖對象目前的 markword 的拷貝孙咪;
- 拷貝對象頭中的 markword 復(fù)制到鎖記錄中;
- 拷貝成功后巡语,虛擬機將使用 CAS 操作嘗試將對象的 markword 更新為指向 Lock Record 的指針翎蹈;
- 如果 CAS 操作失敗,表示存在競爭男公,升級為重量級鎖荤堪,就是操作系統(tǒng)層面的同步方法。在沒有鎖競爭的情況枢赔,輕量級鎖減少互斥產(chǎn)生的性能損耗澄阳。在競爭非常激烈時(輕量級鎖(CAS 操作)總是失敗)踏拜,輕量級鎖會多做很多額外操作碎赢,導(dǎo)致性能下降。
小結(jié)
synchronized 的執(zhí)行過程:
- 檢測 markword 里面是不是當(dāng)前線程的ID速梗,如果是肮塞,表示當(dāng)前線程處于偏向鎖。
- 如果不是姻锁,則使用 CAS 將 markword 中的線程ID更新為當(dāng)前線程的ID枕赵,如果成功則表示當(dāng)前線程獲得偏向鎖。
- 如果失敗位隶,則說明發(fā)生競爭拷窜,撤銷偏向鎖,進(jìn)而升級為輕量級鎖。
- 當(dāng)前線程使用 CAS 將對象的 markword 更新為指向 Lock Record 的指針装黑,如果成功副瀑,當(dāng)前線程獲得鎖。
- 如果失敗恋谭,表示其他線程競爭鎖糠睡,則升級為重量級鎖。
上面幾種鎖都是 JVM 自己內(nèi)部實現(xiàn)疚颊,當(dāng)我們執(zhí)行 synchronized 同步塊的時候 jvm 會根據(jù)啟用的鎖和當(dāng)前線程的爭用情況狈孔,決定如何執(zhí)行同步操作;
在所有的鎖都啟用的情況下線程進(jìn)入臨界區(qū)時會先去獲取偏向鎖材义,如果已經(jīng)存在偏向鎖了均抽,則會嘗試獲取輕量級鎖。如果輕量級鎖獲取失敗其掂,則啟用自旋鎖油挥,如果自旋也沒有獲取到鎖,則升級為重量級鎖款熬,沒有獲取到鎖的線程阻塞掛起深寥,直到持有鎖的線程執(zhí)行完同步塊喚醒他們;
偏向鎖是在無鎖爭用的情況下使用的贤牛,也就是同步開在當(dāng)前線程沒有執(zhí)行完之前惋鹅,沒有其它線程會執(zhí)行該同步塊,一旦有了第二個線程的爭用殉簸,偏向鎖就會升級為輕量級鎖闰集,如果輕量級鎖自旋到達(dá)閾值后,沒有獲取到鎖般卑,就會升級為重量級鎖武鲁;
鎖優(yōu)化
以上介紹的鎖不是我們代碼中能夠控制的,但是借鑒上面的思想椭微,我們可以優(yōu)化我們自己線程的加鎖操作洞坑;
減少鎖時間
不需要同步執(zhí)行的代碼盲链,能不放在同步快里面執(zhí)行就不要放在同步快內(nèi)蝇率,可以讓鎖盡快釋放;
減少鎖的粒度
它的思想是將物理上的一個鎖刽沾,拆成邏輯上的多個鎖本慕,增加并行度,從而降低鎖競爭侧漓。它的思想也是用空間來換時間锅尘;
java中很多數(shù)據(jù)結(jié)構(gòu)都是采用這種方法提高并發(fā)操作的效率:
ConcurrentHashMap
java中的 ConcurrentHashMap 在 jdk1.8 之前的版本,使用一個 Segment 數(shù)組,Segment繼承自 ReenTrantLock藤违,所以每個 Segment 就是個可重入鎖浪腐,每個 Segment 有一個HashEntry< K,V >數(shù)組用來存放數(shù)據(jù),put 操作時顿乒,先確定往哪個 Segment 放數(shù)據(jù)议街,只需要鎖定這個 Segment,執(zhí)行 put璧榄,其它的 Segment 不會被鎖定特漩;所以數(shù)組中有多少個 Segment 就允許同一時刻多少個線程存放數(shù)據(jù),這樣增加了并發(fā)能力骨杂。
LongAdder
LongAdder 實現(xiàn)思路也類似 ConcurrentHashMap涂身,LongAdder 有一個根據(jù)當(dāng)前并發(fā)狀況動態(tài)改變的 Cell 數(shù)組,Cell 對象里面有一個long類型的 value 用來存儲值;
開始沒有并發(fā)爭用的時候或者是 cells 數(shù)組正在初始化的時候搓蚪,會使用cas來將值累加到成員變量的 base 上蛤售,在并發(fā)爭用的情況下,LongAdder 會初始化 cells 數(shù)組妒潭,在 Cell 數(shù)組中選定一個 Cell 加鎖悍抑,數(shù)組有多少個 cell,就允許同時有多少線程進(jìn)行修改杜耙,最后將數(shù)組中每個 Cell 中的 value 相加搜骡,再加上 base 的值,就是最終的值佑女;cell 數(shù)組還能根據(jù)當(dāng)前線程爭用情況進(jìn)行擴容记靡,初始長度為2,每次擴容會增長一倍团驱,直到擴容到大于等于cpu數(shù)量就不再擴容摸吠,這也就是為什么 LongAdder 比 cas 和 AtomicLong 效率要高的原因,后面兩者都是 volatile+cas 實現(xiàn)的嚎花,他們的競爭維度是1寸痢,LongAdder 的競爭維度為“Cell個數(shù)+1”,為什么要+1紊选?因為它還有一個 base啼止,如果競爭不到鎖還會嘗試將數(shù)值加到 base 上;
LinkedBlockingQueue
LinkedBlockingQueue也體現(xiàn)了這樣的思想兵罢,在隊列頭入隊献烦,在隊列尾出隊,入隊和出隊使用不同的鎖卖词,相對于LinkedBlockingArray只有一個鎖效率要高巩那;
拆鎖的粒度不能無限拆,最多可以將一個鎖拆為當(dāng)前 cup 數(shù)量個鎖即可;
鎖粗化
大部分情況下我們是要讓鎖的粒度最小化即横,鎖的粗化則是要增大鎖的粒度;
在以下場景下需要粗化鎖的粒度:
假如有一個循環(huán)噪生,循環(huán)內(nèi)的操作需要加鎖,我們應(yīng)該把鎖放到循環(huán)外面东囚,否則每次進(jìn)出循環(huán)杠园,都進(jìn)出一次臨界區(qū),效率是非常差的舔庶;
使用讀寫鎖
ReentrantReadWriteLock 是一個讀寫鎖抛蚁,讀操作加讀鎖,可以并發(fā)讀惕橙,寫操作使用寫鎖瞧甩,只能單線程寫;
讀寫分離
CopyOnWriteArrayList 弥鹦、CopyOnWriteArraySet
CopyOnWrite 容器即寫時復(fù)制的容器肚逸。通俗的理解是當(dāng)我們往一個容器添加元素的時候,不直接往當(dāng)前容器添加彬坏,而是先將當(dāng)前容器進(jìn)行 Copy朦促,復(fù)制出一個新的容器,然后新的容器里添加元素栓始,添加完元素之后务冕,再將原容器的引用指向新的容器。這樣做的好處是我們可以對 CopyOnWrite 容器進(jìn)行并發(fā)的讀幻赚,而不需要加鎖禀忆,因為當(dāng)前容器不會添加任何元素。所以 CopyOnWrite 容器也是一種讀寫分離的思想落恼,讀和寫不同的容器箩退。
CopyOnWrite 并發(fā)容器用于讀多寫少的并發(fā)場景,因為佳谦,讀的時候沒有鎖戴涝,但是對其進(jìn)行更改的時候是會加鎖的,否則會導(dǎo)致多個線程同時復(fù)制出多個副本钻蔑,各自修改各自的啥刻;
使用 cas
如果需要同步的操作執(zhí)行速度非常快矢棚,并且線程競爭并不激烈郑什,這時候使用 cas 效率會更高府喳,因為加鎖會導(dǎo)致線程的上下文切換蒲肋,如果上下文切換的耗時比同步操作本身更耗時,且線程對資源的競爭不激烈,使用 volatiled+cas 操作會是非常高效的選擇兜粘;