带你玩转关键字Synchronized
synchronized關(guān)鍵字是Java并發(fā)編程中線程同步的常用手段之一,當(dāng)多個(gè)線程同時(shí)訪問某個(gè)線程間的共享變量時(shí),我們可以使用synchronized來保證線程安全。synchronized可以保證互斥性,可見性和有序性:
- 互斥性:確保線程互斥的訪問同步代,鎖自動(dòng)釋放,多個(gè)線程操作同個(gè)代碼塊或函數(shù)必須排隊(duì)獲得鎖;
- 可見性:保證共享變量的修改能夠及時(shí)可見,獲得鎖的線程操作完畢后會(huì)將所數(shù)據(jù)刷新到共享內(nèi)存區(qū);
- 有序性:有效解決指令重排問題。
接下來我們一步一步來了解synchronized的底層實(shí)現(xiàn)原理。
synchronized使用方式
有如下程序,有兩個(gè)線程需要對(duì)共享變量i進(jìn)行加1的操作,每個(gè)線程都加到10000,最終需要輸出20000。
/*** @Author likangmin* @create 2020/12/11 13:36*/ public class Thread4 implements Runnable{//共享資源(臨界資源)static int i=0;public void increase(){i++;}public void run() {for(int j=0;j<10000;j++){increase();}}public static void main(String[] args) throws InterruptedException {Thread4 t=new Thread4();Thread t1=new Thread(t);Thread t2=new Thread(t);t1.start();t2.start();t1.join();//主線程等待t1執(zhí)行完畢t2.join();//主線程等待t2執(zhí)行完畢System.out.println(i);} }在不使用synchronized關(guān)鍵字的時(shí)候,我們看一下最后執(zhí)行的結(jié)果:
發(fā)現(xiàn)最終的結(jié)果是小于20000的,顯然結(jié)果是不正確的。這個(gè)時(shí)候就需要使用synchronized關(guān)鍵字, 一共有三種使用的方法:直接修飾某個(gè)實(shí)例方法,直接修飾某個(gè)靜態(tài)方法,修飾代碼塊。每個(gè)類都有一個(gè)類鎖,類的每個(gè)對(duì)象也有一個(gè)內(nèi)置鎖,它們是互不干擾的,也就是說一個(gè)線程可以同時(shí)獲得類鎖和該類實(shí)例化對(duì)象的內(nèi)置鎖,當(dāng)線程訪問synchronzied修飾的方法時(shí),依據(jù)修飾的不同類型獲取不同的鎖。
修飾實(shí)例方法
synchronized關(guān)鍵詞作用在方法的前面,用來鎖定方法,默認(rèn)鎖定的是this對(duì)象。synchronized修飾的實(shí)例方法,多線程并發(fā)訪問時(shí),只能有一個(gè)線程進(jìn)入,獲得對(duì)象內(nèi)置鎖,其他線程阻塞等待,但在此期間線程仍然可以訪問其他方法。
我們還是以上面的例子來進(jìn)行測試,在實(shí)例方法上加上synchronized:
看一下輸出,的確是20000,符合最終的結(jié)果。
修飾靜態(tài)方法
synchronized修飾在方法上,不過修飾的是靜態(tài)方法,等價(jià)于鎖定的是Class對(duì)象。synchronized修飾的靜態(tài)方法,多線程并發(fā)訪問時(shí),只能有一個(gè)線程進(jìn)入,獲得類鎖,其他線程阻塞等待,但在此期間線程仍然可以訪問其他方法。
我們還是以上面的例子來進(jìn)行測試,在靜態(tài)方法上加上synchronized:
看一下輸出,的確是20000,符合最終的結(jié)果。
修飾代碼塊
在函數(shù)體內(nèi)部對(duì)于要修改的參數(shù)區(qū)間用synchronized來修飾,相比與鎖定函數(shù)這個(gè)范圍更小,可以指定鎖定什么對(duì)象。synchronized修飾的代碼塊,多線程并發(fā)訪問時(shí),只能有一個(gè)線程進(jìn)入,根據(jù)括號(hào)中的對(duì)象或者是類,獲得相應(yīng)的對(duì)象內(nèi)置鎖或者是類鎖。
我們還是以上面的例子來進(jìn)行測試,在代碼塊上加上synchronized:
看一下輸出,的確是20000,符合最終的結(jié)果。
有的同學(xué)可能會(huì)問,synchronized修飾方法和修飾代碼塊有什么區(qū)別?這個(gè)我們?cè)诤竺鏁?huì)解答,請(qǐng)你耐心往后看。
synchronized底層原理
synchronized內(nèi)存模型
講清 synchronized 關(guān)鍵字的原理前需要理清 Java 對(duì)象在內(nèi)存中的表示方法。我們知道在Java的JVM內(nèi)存區(qū)域中一個(gè)對(duì)象在堆區(qū)創(chuàng)建,創(chuàng)建后的對(duì)象由三部分組成:
這三部分功能如下:
- 對(duì)象頭:主要包括兩部分 Klass Point跟 Mark Word,如果是數(shù)組,還包括數(shù)組的長度;
- 實(shí)例變量:存放類的屬性數(shù)據(jù)信息,包括父類的屬性信息,這部分內(nèi)存按4字節(jié)對(duì)齊;
- 填充數(shù)據(jù):由于虛擬機(jī)要求對(duì)象起始地址必須是8字節(jié)的整數(shù)倍。填充數(shù)據(jù)不是必須存在的,僅僅是為了字節(jié)對(duì)齊。
synchronized不論是修飾方法還是代碼塊,都是通過持有修飾對(duì)象的鎖來實(shí)現(xiàn)同步,synchronized鎖對(duì)象是存在對(duì)象頭Mark Word。
Mark Word 中的某些字段發(fā)生變化,就可以代表鎖不同的狀態(tài)。Mark Word狀態(tài)表示位如下:
其中輕量級(jí)鎖和偏向鎖是Java6對(duì)synchronized鎖進(jìn)行優(yōu)化后新增加的,這里我們主要分析一下重量級(jí)鎖也就是通常說synchronized的對(duì)象鎖,鎖標(biāo)識(shí)位為10,其中指針指向的是monitor對(duì)象(也稱為管程或監(jiān)視器鎖)的起始地址。
每個(gè)對(duì)象都存在著一個(gè) monitor與之關(guān)聯(lián),而monitor可以被線程擁有或釋放,在Java虛擬機(jī)(HotSpot)中,monitor是由ObjectMonitor實(shí)現(xiàn)的,其主要數(shù)據(jù)結(jié)構(gòu)如下(位于HotSpot虛擬機(jī)源碼ObjectMonitor.hpp文件,C++實(shí)現(xiàn)的):
monitor運(yùn)行圖如下:
依據(jù)以上圖示我們簡單梳理一下執(zhí)行過程:
因?yàn)楸O(jiān)視器鎖(monitor)是依賴于底層的操作系統(tǒng)的Mutex Lock來實(shí)現(xiàn)的,而操作系統(tǒng)實(shí)現(xiàn)線程之間的切換時(shí)需要從用戶態(tài)轉(zhuǎn)換到核心態(tài),這個(gè)狀態(tài)之間的轉(zhuǎn)換需要相對(duì)比較長的時(shí)間,時(shí)間成本相對(duì)較高,這也是早期的synchronized效率低的原因。在Java 6之后Java官方對(duì)從JVM層面對(duì)synchronized較大優(yōu)化最終提升顯著,Java 6之后,為了減少獲得鎖和釋放鎖所帶來的性能消耗,引入了鎖升級(jí)的概念。
synchronizedd修改方法與代碼塊區(qū)別
在介紹鎖升級(jí)之前,我們?cè)倩氐阶铋_始一個(gè)問題,synchronized修飾方法和修飾代碼塊有什么區(qū)別。針對(duì)這兩種情況,Java 編譯時(shí)的處理方法并不相同。我們可以通過反匯編看下同步方法跟同步方法塊在匯編語言級(jí)別是什么樣的指令。
對(duì)之前的Thread1,即synchronized修改方法的類,在終端執(zhí)行javap -v Thread1.calss,得到的字節(jié)碼文件部分如下:
對(duì)之前的Thread3,即synchronized修改代碼塊,在終端執(zhí)行javap -v Thread1.calss,得到的字節(jié)碼文件部分如下:
我們可以看到:
- 第一種情況,編譯器會(huì)為其自動(dòng)生成了一個(gè) ACC_SYNCHRONIZED 關(guān)鍵字用來標(biāo)識(shí)。在 JVM 進(jìn)行方法調(diào)用時(shí),當(dāng)發(fā)現(xiàn)調(diào)用的方法被 ACC_SYNCHRONIZED 修飾,則會(huì)先嘗試獲得鎖,然后開始執(zhí)行方法,方法執(zhí)行之后再釋放監(jiān)視器鎖。這時(shí)如果其他線程來請(qǐng)求執(zhí)行方法,會(huì)因?yàn)闊o法獲得監(jiān)視器鎖而被阻斷住。值得注意的是,如果在方法執(zhí)行過程中,發(fā)生了異常,并且方法內(nèi)部并沒有處理該異常,那么在異常被拋到方法外面之前監(jiān)視器鎖會(huì)被自動(dòng)釋放。
- 第二種情況,編譯時(shí)在代碼塊開始前生成對(duì)應(yīng)的1個(gè) monitorenter 指令,代表同步塊進(jìn)入。2個(gè) monitorexit 指令,前一個(gè)代表同步塊正常退出,后一個(gè)在同步塊異常時(shí)退出。每個(gè)對(duì)象維護(hù)著一個(gè)記錄著被鎖次數(shù)的計(jì)數(shù)器。未被鎖定的對(duì)象的該計(jì)數(shù)器為0,當(dāng)一個(gè)線程獲得鎖(執(zhí)行monitorenter)后,該計(jì)數(shù)器自增變?yōu)?1 ,當(dāng)同一個(gè)線程再次獲得該對(duì)象的鎖的時(shí)候,計(jì)數(shù)器再次自增。當(dāng)同一個(gè)線程釋放鎖(執(zhí)行monitorexit指令)的時(shí)候,計(jì)數(shù)器再自減。當(dāng)計(jì)數(shù)器為0的時(shí)候。鎖將被釋放,其他線程便可以獲得鎖。
CAS算法
在講其他之前,我們還要給大家介紹一個(gè)算法- CAS 算法。CAS 算法全稱為 Compare And Swap。顧名思義,該算法涉及到了兩個(gè)操作,比較(Compare)和交換(Swap)。其基本流程如下圖:
在對(duì)共享變量進(jìn)行多線程操作的時(shí)候,難免會(huì)出現(xiàn)線程安全問題。對(duì)該問題的一種解決策略就是對(duì)該變量加鎖,保證該變量在某個(gè)時(shí)間段只能被一個(gè)線程操作。但是這種方式的系統(tǒng)開銷比較大,因此提出了一種新的算法-CAS 算法。算法的思路如下:
當(dāng)線程運(yùn)行 CAS 算法時(shí),該運(yùn)行過程是原子操作,原子操作的含義就是線程開始跑這個(gè)函數(shù)后,運(yùn)行過程中不會(huì)被別的程序打斷。
鎖升級(jí)
synchronized鎖有四種狀態(tài),無鎖、偏向鎖、輕量級(jí)鎖、重量級(jí)鎖。這幾個(gè)狀態(tài)會(huì)隨著競爭狀態(tài)逐漸升級(jí),鎖可以升級(jí)但不能降級(jí),但是偏向鎖狀態(tài)可以被重置為無鎖狀態(tài)。
偏向鎖
HotSpot的作者大量研究發(fā)現(xiàn)大多數(shù)時(shí)候是不存在鎖競爭的,經(jīng)常是一個(gè)線程多次獲得同一個(gè)鎖,因此如果每次都要競爭鎖會(huì)增大很多沒有必要付出的代價(jià),為了降低獲取鎖的代價(jià),引入偏向鎖。以下為偏向鎖的 Mark Word 字段。
如果一個(gè)線程獲得了鎖,那么鎖就進(jìn)入偏向模式,此時(shí)Mark Word 的結(jié)構(gòu)也變?yōu)槠蜴i結(jié)構(gòu)。當(dāng)這個(gè)線程再次請(qǐng)求鎖時(shí),無需再做任何同步操作,即不用再去重復(fù)獲取鎖的過程,這樣就省去了大量有關(guān)鎖申請(qǐng)的操作,從而也就提供程序的性能。偏向鎖的申請(qǐng)流程:
偏向鎖使用場景
- 對(duì)于沒有鎖競爭的場合,偏向鎖有很好的優(yōu)化效果,畢竟極有可能連續(xù)多次是同一個(gè)線程申請(qǐng)相同的鎖;
- 對(duì)于鎖競爭比較激烈的場合,偏向鎖就失效了,因?yàn)檫@樣場合極有可能每次申請(qǐng)鎖的線程都是不相同的,因此這種場合下不應(yīng)該使用偏向鎖,否則會(huì)得不償失;
- 偏向鎖失敗后,并不會(huì)立即膨脹為重量級(jí)鎖,而是先升級(jí)為輕量級(jí)鎖。
輕量級(jí)鎖
輕量級(jí)鎖考慮的是競爭鎖對(duì)象的線程不多,而且線程持有鎖的時(shí)間也不長的情景。因?yàn)樽枞€程需要高昂的耗時(shí)實(shí)現(xiàn)CPU從用戶態(tài)轉(zhuǎn)到內(nèi)核態(tài)的切換,如果剛剛阻塞不久這個(gè)鎖就被釋放了,那這個(gè)代價(jià)就有點(diǎn)得不償失了,因此這個(gè)時(shí)候就干脆不阻塞這個(gè)線程,讓它自旋這等待鎖釋放。如果當(dāng)前對(duì)象是輕量級(jí)鎖狀態(tài),對(duì)象的 Mark Word 如下圖所示:
該對(duì)象頭Mark Word分為兩個(gè)部分。第一部分是指向棧中的鎖記錄的指針,第二部分是鎖標(biāo)記位,針對(duì)輕量級(jí)鎖該標(biāo)記位為 00。輕量級(jí)鎖的申請(qǐng)流程:
重量級(jí)鎖
在 Java 的早期版本中,synchronized 鎖屬于重量級(jí)鎖,此時(shí)對(duì)象的 Mark Word 如圖所示:
該對(duì)象頭的 Mark Word 分為兩個(gè)部分,第一部分是指向重量級(jí)鎖的指針,第二部分是鎖標(biāo)記位。這里所說的指向重量級(jí)鎖的指針就是 monitor,monitor 是監(jiān)視器,Java 中每個(gè)對(duì)象會(huì)對(duì)應(yīng)一個(gè)監(jiān)視器,這個(gè)監(jiān)視器其實(shí)也就是監(jiān)控鎖有沒有釋放,釋放的話會(huì)通知下一個(gè)等待鎖的線程去獲取。monitor 的成員變量比較多,我們可以將 monitor 簡單理解成兩部分,第一部分表示當(dāng)前占用鎖的線程,第二部分是等待這把鎖的線程隊(duì)列。
如果當(dāng)前占用鎖的線程把鎖釋放了,那就需要在線程隊(duì)列中喚醒下一個(gè)等待鎖的線程。但是阻塞或喚醒一個(gè)線程需要依賴底層的操作系統(tǒng)來實(shí)現(xiàn),Java 的線程是映射到操作系統(tǒng)的原生線程之上的。而操作系統(tǒng)實(shí)現(xiàn)線程之間的切換需要從用戶態(tài)轉(zhuǎn)換到核心態(tài),這個(gè)狀態(tài)轉(zhuǎn)換需要花費(fèi)很多的處理器時(shí)間,甚至可能比用戶代碼執(zhí)行的時(shí)間還要長。由于這種效率太低,所以提出了偏向鎖,輕量級(jí)鎖等的改進(jìn)優(yōu)化。
總結(jié)
最后給大家總結(jié)一下各個(gè)鎖的優(yōu)缺點(diǎn)以及各自適用的場景:
| 偏向鎖 | 加鎖解鎖無需額外消耗,跟非同步方法時(shí)間相差納秒級(jí)別 | 如果競爭線程多,會(huì)帶來額外的鎖撤銷的消耗 | 基本沒有其他線程競爭的同步場景 |
| 輕量級(jí)鎖 | 競爭的線程不會(huì)阻塞而是在自旋,可提高程序響應(yīng)速度 | 如果一直無法獲得會(huì)自旋消耗CPU | 少量線程競爭,持有鎖時(shí)間不長,追求響應(yīng)速度 |
| 重量級(jí)鎖 | 線程競爭不會(huì)導(dǎo)致CPU自旋跟消耗CPU資源 | 線程阻塞,響應(yīng)時(shí)間長 | 很多線程競爭鎖,切鎖持有時(shí)間長,追求吞吐量時(shí)候 |
最后再奉上鎖升級(jí)大圖(感謝unbelievableme大神的繪制):
想看更多文章,請(qǐng)點(diǎn)擊此處
參考文章:
https://www.cnblogs.com/kundeg/p/8422557.html
總結(jié)
以上是生活随笔為你收集整理的带你玩转关键字Synchronized的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java的TheadLocal使用
- 下一篇: 教你用BitMap排序、查找和存储大量数