超多干貨!synchronized 知識(shí)揭秘

大家好,我是躍哥。synchronized 平時(shí)大家用的多嗎?大家知道他的作用嗎?知道如何更好的使用嗎?今天,就接著 cxuan 的文章,我們一起來(lái)揭秘。
synchronized 這個(gè)關(guān)鍵字的重要性不言而喻,幾乎可以說(shuō)是并發(fā)、多線程必須會(huì)問(wèn)到的關(guān)鍵字了。synchronized 會(huì)涉及到鎖、升級(jí)降級(jí)操作、鎖的撤銷、對(duì)象頭等。所以理解 synchronized 非常重要,本篇文章就帶你從 synchronized 的基本用法、再到 synchronized 的深入理解,對(duì)象頭等,為你揭開(kāi) synchronized 的面紗。
淺析 synchronized
synchronized?是 Java?并發(fā)模塊?非常重要的關(guān)鍵字,它是 Java 內(nèi)建的一種同步機(jī)制,代表了某種內(nèi)在鎖定的概念,當(dāng)一個(gè)線程對(duì)某個(gè)共享資源加鎖后,其他想要獲取共享資源的線程必須進(jìn)行等待,synchronized 也具有互斥和排他的語(yǔ)義。
什么是互斥?我們想必小時(shí)候都玩兒過(guò)磁鐵,磁鐵會(huì)有正負(fù)極的概念,同性相斥異性相吸,相斥相當(dāng)于就是一種互斥的概念,也就是兩者互不相容。
synchronized 也是一種獨(dú)占的關(guān)鍵字,但是它這種獨(dú)占的語(yǔ)義更多的是為了增加線程安全性,通過(guò)獨(dú)占某個(gè)資源以達(dá)到互斥、排他的目的。
在了解了排他和互斥的語(yǔ)義后,我們先來(lái)看一下 synchronized 的用法,先來(lái)了解用法,再來(lái)了解底層實(shí)現(xiàn)。
synchronized 的使用
關(guān)于 synchronized 想必你應(yīng)該都大致了解過(guò)
synchronized 修飾實(shí)例方法,相當(dāng)于是對(duì)類的實(shí)例進(jìn)行加鎖,進(jìn)入同步代碼前需要獲得當(dāng)前實(shí)例的鎖
synchronized 修飾靜態(tài)方法,相當(dāng)于是對(duì)類對(duì)象進(jìn)行加鎖
synchronized 修飾代碼塊,相當(dāng)于是給對(duì)象進(jìn)行加鎖,在進(jìn)入代碼塊前需要先獲得對(duì)象的鎖
下面我們針對(duì)每個(gè)用法進(jìn)行解釋
synchronized 修飾實(shí)例方法
synchronized 修飾實(shí)例方法,實(shí)例方法是屬于類的實(shí)例。synchronized 修飾的實(shí)例方法相當(dāng)于是對(duì)象鎖。下面是一個(gè) synchronized 修飾實(shí)例方法的例子。
public?synchronized?void?method()
{
???//?...
}
像如上述 synchronized 修飾的方法就是實(shí)例方法,下面我們通過(guò)一個(gè)完整的例子來(lái)認(rèn)識(shí)一下 synchronized 修飾實(shí)例方法
public?class?TSynchronized?implements?Runnable{
????static?int?i?=?0;
????public?synchronized?void?increase(){
????????i++;
????????System.out.println(Thread.currentThread().getName());
????}
????@Override
????public?void?run()?{
????????for(int?i?=?0;i?<?1000;i++)?{
????????????increase();
????????}
????}
????public?static?void?main(String[]?args)?throws?InterruptedException?{
????????TSynchronized?tSynchronized?=?new?TSynchronized();
????????Thread?aThread?=?new?Thread(tSynchronized);
????????Thread?bThread?=?new?Thread(tSynchronized);
????????aThread.start();
????????bThread.start();
????????aThread.join();
????????bThread.join();
????????System.out.println("i?=?"?+?i);
????}
}
上面輸出的結(jié)果 i = 2000 ,并且每次都會(huì)打印當(dāng)前現(xiàn)成的名字
來(lái)解釋一下上面代碼,代碼中的 i 是一個(gè)靜態(tài)變量,靜態(tài)變量也是全局變量,靜態(tài)變量存儲(chǔ)在方法區(qū)中。increase 方法由 synchronized 關(guān)鍵字修飾,但是沒(méi)有使用 static 關(guān)鍵字修飾,表示 increase 方法是一個(gè)實(shí)例方法,每次創(chuàng)建一個(gè) TSynchronized 類的同時(shí)都會(huì)創(chuàng)建一個(gè) increase 方法,increase 方法中只是打印出來(lái)了當(dāng)前訪問(wèn)的線程名稱。Synchronized 類實(shí)現(xiàn)了 Runnable 接口,重寫(xiě)了 run 方法,run 方法里面就是一個(gè) 0 - 1000 的計(jì)數(shù)器,這個(gè)沒(méi)什么好說(shuō)的。在 main 方法中,new 出了兩個(gè)線程,分別是 aThread 和 bThread,Thread.join 表示等待這個(gè)線程處理結(jié)束。這段代碼主要的作用就是判斷 synchronized 修飾的方法能夠具有獨(dú)占性。
synchronized 修飾靜態(tài)方法
synchronized 修飾靜態(tài)方法就是 synchronized 和 static 關(guān)鍵字一起使用
public?static?synchronized?void?increase(){}
當(dāng) synchronized 作用于靜態(tài)方法時(shí),表示的就是當(dāng)前類的鎖,因?yàn)殪o態(tài)方法是屬于類的,它不屬于任何一個(gè)實(shí)例成員,因此可以通過(guò) class 對(duì)象控制并發(fā)訪問(wèn)。
這里需要注意一點(diǎn),因?yàn)?synchronized 修飾的實(shí)例方法是屬于實(shí)例對(duì)象,而 synchronized 修飾的靜態(tài)方法是屬于類對(duì)象,所以調(diào)用 synchronized 的實(shí)例方法并不會(huì)阻止訪問(wèn) synchronized 的靜態(tài)方法。
synchronized 修飾代碼塊
synchronized 除了修飾實(shí)例方法和靜態(tài)方法外,synchronized 還可用于修飾代碼塊,代碼塊可以嵌套在方法體的內(nèi)部使用。
public?void?run()?{
??synchronized(obj){
????for(int?j?=?0;j?<?1000;j++){
??????i++;
????}
??}
}
上面代碼中將 obj 作為鎖對(duì)象對(duì)其加鎖,每次當(dāng)線程進(jìn)入 synchronized 修飾的代碼塊時(shí)就會(huì)要求當(dāng)前線程持有obj 實(shí)例對(duì)象鎖,如果當(dāng)前有其他線程正持有該對(duì)象鎖,那么新到的線程就必須等待。
synchronized 修飾的代碼塊,除了可以鎖定對(duì)象之外,也可以對(duì)當(dāng)前實(shí)例對(duì)象鎖、class 對(duì)象鎖進(jìn)行鎖定
//?實(shí)例對(duì)象鎖
synchronized(this){
????for(int?j?=?0;j?<?1000;j++){
????????i++;
????}
}
//class對(duì)象鎖
synchronized(TSynchronized.class){
????for(int?j?=?0;j?<?1000;j++){
????????i++;
????}
}
synchronized 底層原理
在簡(jiǎn)單介紹完 synchronized 之后,我們就來(lái)聊一下 synchronized 的底層原理了。
我們或許都有所了解(下文會(huì)細(xì)致分析),synchronized 的代碼塊是由一組 monitorenter/monitorexit 指令實(shí)現(xiàn)的。而Monitor?對(duì)象是實(shí)現(xiàn)同步的基本單元。
啥是?
Monitor?對(duì)象呢?
Monitor 對(duì)象
任何對(duì)象都關(guān)聯(lián)了一個(gè)管程,管程就是控制對(duì)象并發(fā)訪問(wèn)的一種機(jī)制。管程?是一種同步原語(yǔ),在 Java 中指的就是 synchronized,可以理解為 synchronized 就是 Java 中對(duì)管程的實(shí)現(xiàn)。
管程提供了一種排他訪問(wèn)機(jī)制,這種機(jī)制也就是?互斥?;コ獗WC了在每個(gè)時(shí)間點(diǎn)上,最多只有一個(gè)線程會(huì)執(zhí)行同步方法。
所以你理解了 Monitor 對(duì)象其實(shí)就是使用管程控制同步訪問(wèn)的一種對(duì)象。
對(duì)象內(nèi)存布局
在?hotspot?虛擬機(jī)中,對(duì)象在內(nèi)存中的布局分為三塊區(qū)域:
對(duì)象頭(Header)實(shí)例數(shù)據(jù)(Instance Data)對(duì)齊填充(Padding)
這三塊區(qū)域的內(nèi)存分布如下圖所示

我們來(lái)詳細(xì)介紹一下上面對(duì)象中的內(nèi)容。
對(duì)象頭 Header
對(duì)象頭 Header 主要包含 MarkWord 和對(duì)象指針 Klass Pointer,如果是數(shù)組的話,還要包含數(shù)組的長(zhǎng)度。

在 32 位的虛擬機(jī)中 MarkWord ,Klass Pointer 和數(shù)組長(zhǎng)度分別占用 32 位,也就是 4 字節(jié)。
如果是 64 位虛擬機(jī)的話,MarkWord ,Klass Pointer 和數(shù)組長(zhǎng)度分別占用 64 位,也就是 8 字節(jié)。
在 32 位虛擬機(jī)和 64 位虛擬機(jī)的 Mark Word 所占用的字節(jié)大小不一樣,32 位虛擬機(jī)的 Mark Word 和 Klass Pointer 分別占用 32 bits 的字節(jié),而 64 位虛擬機(jī)的 Mark Word 和 Klass Pointer 占用了64 bits 的字節(jié),下面我們以 32 位虛擬機(jī)為例,來(lái)看一下其 Mark Word 的字節(jié)具體是如何分配的。


用中文翻譯過(guò)來(lái)就是

無(wú)狀態(tài)也就是
無(wú)鎖的時(shí)候,對(duì)象頭開(kāi)辟 25 bit 的空間用來(lái)存儲(chǔ)對(duì)象的 hashcode ,4 bit 用于存放分代年齡,1 bit 用來(lái)存放是否偏向鎖的標(biāo)識(shí)位,2 bit 用來(lái)存放鎖標(biāo)識(shí)位為 01。偏向鎖?中劃分更細(xì),還是開(kāi)辟 25 bit 的空間,其中 23 bit 用來(lái)存放線程ID,2bit 用來(lái)存放 epoch,4bit 存放分代年齡,1 bit 存放是否偏向鎖標(biāo)識(shí), 0 表示無(wú)鎖,1 表示偏向鎖,鎖的標(biāo)識(shí)位還是 01。輕量級(jí)鎖中直接開(kāi)辟 30 bit 的空間存放指向棧中鎖記錄的指針,2bit 存放鎖的標(biāo)志位,其標(biāo)志位為 00。重量級(jí)鎖中和輕量級(jí)鎖一樣,30 bit 的空間用來(lái)存放指向重量級(jí)鎖的指針,2 bit 存放鎖的標(biāo)識(shí)位,為 11GC標(biāo)記開(kāi)辟 30 bit 的內(nèi)存空間卻沒(méi)有占用,2 bit 空間存放鎖標(biāo)志位為 11。
其中無(wú)鎖和偏向鎖的鎖標(biāo)志位都是 01,只是在前面的 1 bit 區(qū)分了這是無(wú)鎖狀態(tài)還是偏向鎖狀態(tài)。
關(guān)于為什么這么分配的內(nèi)存,我們可以從?OpenJDK?中的markOop.hpp類中的枚舉窺出端倪

來(lái)解釋一下
age_bits 就是我們說(shuō)的分代回收的標(biāo)識(shí),占用4字節(jié)
lock_bits 是鎖的標(biāo)志位,占用2個(gè)字節(jié)
biased_lock_bits 是是否偏向鎖的標(biāo)識(shí),占用1個(gè)字節(jié)。
max_hash_bits 是針對(duì)無(wú)鎖計(jì)算的 hashcode 占用字節(jié)數(shù)量,如果是 32 位虛擬機(jī),就是 32 - 4 - 2 -1 = 25 byte,如果是 64 位虛擬機(jī),64 - 4 - 2 - 1 = 57 byte,但是會(huì)有 25 字節(jié)未使用,所以 64 位的 hashcode 占用 31 byte。
hash_bits 是針對(duì) 64 位虛擬機(jī)來(lái)說(shuō),如果最大字節(jié)數(shù)大于 31,則取 31,否則取真實(shí)的字節(jié)數(shù)
cms_bits 我覺(jué)得應(yīng)該是不是 64 位虛擬機(jī)就占用 0 byte,是 64 位就占用 1byte
epoch_bits 就是 epoch 所占用的字節(jié)大小,2 字節(jié)。
在上面的虛擬機(jī)對(duì)象頭分配表中,我們可以看到有幾種鎖的狀態(tài):無(wú)鎖(無(wú)狀態(tài)),偏向鎖,輕量級(jí)鎖,重量級(jí)鎖,其中輕量級(jí)鎖和偏向鎖是 JDK1.6 中對(duì) synchronized 鎖進(jìn)行優(yōu)化后新增加的,其目的就是為了大大優(yōu)化鎖的性能,所以在 JDK 1.6 中,使用 synchronized 的開(kāi)銷也沒(méi)那么大了。其實(shí)從鎖有無(wú)鎖定來(lái)講,還是只有無(wú)鎖和重量級(jí)鎖,偏向鎖和輕量級(jí)鎖的出現(xiàn)就是增加了鎖的獲取性能而已,并沒(méi)有出現(xiàn)新的鎖。
所以我們的重點(diǎn)放在對(duì) synchronized 重量級(jí)鎖的研究上,當(dāng) monitor 被某個(gè)線程持有后,它就會(huì)處于鎖定狀態(tài)。在 HotSpot 虛擬機(jī)中,monitor 的底層代碼是由?ObjectMonitor?實(shí)現(xiàn)的,其主要數(shù)據(jù)結(jié)構(gòu)如下(位于 HotSpot 虛擬機(jī)源碼 ObjectMonitor.hpp 文件,C++ 實(shí)現(xiàn)的)

這段 C++ 中需要注意幾個(gè)屬性:_WaitSet 、 _EntryList 和 _Owner,每個(gè)等待獲取鎖的線程都會(huì)被封裝稱為?ObjectWaiter?對(duì)象。

_Owner 是指向了 ObjectMonitor 對(duì)象的線程,而 _WaitSet 和 _EntryList 就是用來(lái)保存每個(gè)線程的列表。
那么這兩個(gè)列表有什么區(qū)別呢?這個(gè)問(wèn)題我和你聊一下鎖的獲取流程你就清楚了。
鎖的兩個(gè)列表
當(dāng)多個(gè)線程同時(shí)訪問(wèn)某段同步代碼時(shí),首先會(huì)進(jìn)入 _EntryList 集合,當(dāng)線程獲取到對(duì)象的 monitor 之后,就會(huì)進(jìn)入 _Owner 區(qū)域,并把 ObjectMonitor 對(duì)象的 _Owner 指向?yàn)楫?dāng)前線程,并使 _count + 1,如果調(diào)用了釋放鎖(比如 wait)的操作,就會(huì)釋放當(dāng)前持有的 monitor ,owner = null, _count - 1,同時(shí)這個(gè)線程會(huì)進(jìn)入到 _WaitSet 列表中等待被喚醒。如果當(dāng)前線程執(zhí)行完畢后也會(huì)釋放 monitor 鎖,只不過(guò)此時(shí)不會(huì)進(jìn)入 _WaitSet 列表了,而是直接復(fù)位 _count 的值。

Klass Pointer 表示的是類型指針,也就是對(duì)象指向它的類元數(shù)據(jù)的指針,虛擬機(jī)通過(guò)這個(gè)指針來(lái)確定這個(gè)對(duì)象是哪個(gè)類的實(shí)例。
你可能不是很理解指針是個(gè)什么概念,你可以簡(jiǎn)單理解為指針就是指向某個(gè)數(shù)據(jù)的地址。

實(shí)例數(shù)據(jù) Instance Data
實(shí)例數(shù)據(jù)部分是對(duì)象真正存儲(chǔ)的有效信息,也是代碼中定義的各個(gè)字段的字節(jié)大小,比如一個(gè) byte 占 1 個(gè)字節(jié),一個(gè) int 占用 4 個(gè)字節(jié)。
對(duì)齊 Padding
對(duì)齊不是必須存在的,它只起到了占位符(%d, %c 等)的作用。這就是 JVM 的要求了,因?yàn)?HotSpot JVM 要求對(duì)象的起始地址必須是 8 字節(jié)的整數(shù)倍,也就是說(shuō)對(duì)象的字節(jié)大小是 8 的整數(shù)倍,不夠的需要使用 Padding 補(bǔ)全。
鎖的升級(jí)流程
先來(lái)個(gè)大體的流程圖來(lái)感受一下這個(gè)過(guò)程,然后下面我們?cè)俜珠_(kāi)來(lái)說(shuō)

無(wú)鎖
無(wú)鎖狀態(tài),無(wú)鎖即沒(méi)有對(duì)資源進(jìn)行鎖定,所有的線程都可以對(duì)同一個(gè)資源進(jìn)行訪問(wèn),但是只有一個(gè)線程能夠成功修改資源。

無(wú)鎖的特點(diǎn)就是在循環(huán)內(nèi)進(jìn)行修改操作,線程會(huì)不斷的嘗試修改共享資源,直到能夠成功修改資源并退出,在此過(guò)程中沒(méi)有出現(xiàn)沖突的發(fā)生,這很像我們?cè)谥拔恼轮薪榻B的 CAS 實(shí)現(xiàn),CAS 的原理和應(yīng)用就是無(wú)鎖的實(shí)現(xiàn)。無(wú)鎖無(wú)法全面代替有鎖,但無(wú)鎖在某些場(chǎng)合下的性能是非常高的。
偏向鎖
HotSpot 的作者經(jīng)過(guò)研究發(fā)現(xiàn),大多數(shù)情況下,鎖不僅不存在多線程競(jìng)爭(zhēng),還存在鎖由同一線程多次獲得的情況,偏向鎖就是在這種情況下出現(xiàn)的,它的出現(xiàn)是為了解決只有在一個(gè)線程執(zhí)行同步時(shí)提高性能。

可以從對(duì)象頭的分配中看到,偏向鎖要比無(wú)鎖多了線程ID?和?epoch,下面我們就來(lái)描述一下偏向鎖的獲取過(guò)程
偏向鎖獲取過(guò)程
首先線程訪問(wèn)同步代碼塊,會(huì)通過(guò)檢查對(duì)象頭 Mark Word 的
鎖標(biāo)志位判斷目前鎖的狀態(tài),如果是 01,說(shuō)明就是無(wú)鎖或者偏向鎖,然后再根據(jù)是否偏向鎖?的標(biāo)示判斷是無(wú)鎖還是偏向鎖,如果是無(wú)鎖情況下,執(zhí)行下一步線程使用 CAS 操作來(lái)嘗試對(duì)對(duì)象加鎖,如果使用 CAS 替換 ThreadID 成功,就說(shuō)明是第一次上鎖,那么當(dāng)前線程就會(huì)獲得對(duì)象的偏向鎖,此時(shí)會(huì)在對(duì)象頭的 Mark Word 中記錄當(dāng)前線程 ID 和獲取鎖的時(shí)間 epoch 等信息,然后執(zhí)行同步代碼塊。
全局安全點(diǎn)(Safe Point):全局安全點(diǎn)的理解會(huì)涉及到 C 語(yǔ)言底層的一些知識(shí),這里簡(jiǎn)單理解 SafePoint 是 Java 代碼中的一個(gè)線程可能暫停執(zhí)行的位置。
等到下一次線程在進(jìn)入和退出同步代碼塊時(shí)就不需要進(jìn)行?CAS?操作進(jìn)行加鎖和解鎖,只需要簡(jiǎn)單判斷一下對(duì)象頭的 Mark Word 中是否存儲(chǔ)著指向當(dāng)前線程的線程ID,判斷的標(biāo)志當(dāng)然是根據(jù)鎖的標(biāo)志位來(lái)判斷的。如果用流程圖來(lái)表示的話就是下面這樣

關(guān)閉偏向鎖
偏向鎖在Java 6 和Java 7 里是默認(rèn)啟用的。由于偏向鎖是為了在只有一個(gè)線程執(zhí)行同步塊時(shí)提高性能,如果你確定應(yīng)用程序里所有的鎖通常情況下處于競(jìng)爭(zhēng)狀態(tài),可以通過(guò)JVM參數(shù)關(guān)閉偏向鎖:-XX:-UseBiasedLocking=false,那么程序默認(rèn)會(huì)進(jìn)入輕量級(jí)鎖狀態(tài)。
關(guān)于 epoch
偏向鎖的對(duì)象頭中有一個(gè)被稱為?epoch?的值,它作為偏差有效性的時(shí)間戳。
輕量級(jí)鎖
輕量級(jí)鎖是指當(dāng)前鎖是偏向鎖的時(shí)候,資源被另外的線程所訪問(wèn),那么偏向鎖就會(huì)升級(jí)為輕量級(jí)鎖,其他線程會(huì)通過(guò)自旋的形式嘗試獲取鎖,不會(huì)阻塞,從而提高性能,下面是詳細(xì)的獲取過(guò)程。
輕量級(jí)鎖加鎖過(guò)程
緊接著上一步,如果 CAS 操作替換 ThreadID 沒(méi)有獲取成功,執(zhí)行下一步
如果使用 CAS 操作替換 ThreadID 失?。ㄟ@時(shí)候就切換到另外一個(gè)線程的角度)說(shuō)明該資源已被同步訪問(wèn)過(guò),這時(shí)候就會(huì)執(zhí)行鎖的撤銷操作,撤銷偏向鎖,然后等原持有偏向鎖的線程到達(dá)
全局安全點(diǎn)(SafePoint)時(shí),會(huì)暫停原持有偏向鎖的線程,然后會(huì)檢查原持有偏向鎖的狀態(tài),如果已經(jīng)退出同步,就會(huì)喚醒持有偏向鎖的線程,執(zhí)行下一步檢查對(duì)象頭中的 Mark Word 記錄的是否是當(dāng)前線程 ID,如果是,執(zhí)行同步代碼,如果不是,執(zhí)行偏向鎖獲取流程?的第2步。
如果用流程表示的話就是下面這樣(已經(jīng)包含偏向鎖的獲?。?/p>
重量級(jí)鎖
重量級(jí)鎖其實(shí)就是 synchronized 最終加鎖的過(guò)程,在 JDK 1.6 之前,就是由無(wú)鎖 -> 加鎖的這個(gè)過(guò)程。
重量級(jí)鎖的獲取流程
接著上面偏向鎖的獲取過(guò)程,由偏向鎖升級(jí)為輕量級(jí)鎖,執(zhí)行下一步
會(huì)在原持有偏向鎖的線程的棧中分配鎖記錄,將對(duì)象頭中的 Mark Word 拷貝到原持有偏向鎖線程的記錄中,原持有偏向鎖的線程獲得輕量級(jí)鎖,然后喚醒原持有偏向鎖的線程,從安全點(diǎn)處繼續(xù)執(zhí)行,執(zhí)行完畢后,執(zhí)行下一步,當(dāng)前線程執(zhí)行第 4 步
執(zhí)行完畢后,開(kāi)始輕量級(jí)解鎖操作,解鎖需要判斷兩個(gè)條件
判斷對(duì)象頭中的 Mark Word 中鎖記錄指針是否指向當(dāng)前棧中記錄的指針

拷貝在當(dāng)前線程鎖記錄的 Mark Word 信息是否與對(duì)象頭中的 Mark Word 一致。
如果上面兩個(gè)判斷條件都符合的話,就進(jìn)行鎖釋放,如果其中一個(gè)條件不符合,就會(huì)釋放鎖,并喚起等待的線程,進(jìn)行新一輪的鎖競(jìng)爭(zhēng)。
在當(dāng)前線程的棧中分配鎖記錄,拷貝對(duì)象頭中的 MarkWord 到當(dāng)前線程的鎖記錄中,執(zhí)行 CAS 加鎖操作,會(huì)把對(duì)象頭 Mark Word 中鎖記錄指針指向當(dāng)前線程鎖記錄,如果成功,獲取輕量級(jí)鎖,執(zhí)行同步代碼,然后執(zhí)行第3步,如果不成功,執(zhí)行下一步
當(dāng)前線程沒(méi)有使用 CAS 成功獲取鎖,就會(huì)自旋一會(huì)兒,再次嘗試獲取,如果在多次自旋到達(dá)上限后還沒(méi)有獲取到鎖,那么輕量級(jí)鎖就會(huì)升級(jí)為?
重量級(jí)鎖

如果用流程圖表示是這樣的

根據(jù)上面對(duì)于鎖升級(jí)細(xì)致的描述,我們可以總結(jié)一下不同鎖的適用范圍和場(chǎng)景。

synchronized 代碼塊的底層實(shí)現(xiàn)
為了便于方便研究,我們把 synchronized 修飾代碼塊的示例簡(jiǎn)單化,如下代碼所示
public?class?SynchronizedTest?{
????private?int?i;
????public?void?syncTask(){
????????synchronized?(this){
????????????i++;
????????}
????}
}
我們主要關(guān)注一下 synchronized 的字節(jié)碼,如下所示

從這段字節(jié)碼中我們可以知道,同步語(yǔ)句塊使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代碼塊的開(kāi)始位置,monitorexit 指令指向同步代碼塊的結(jié)束位置。
那么為什么會(huì)有兩個(gè) monitorexit 呢?
不知道你注意到下面的異常表了嗎?如果你不知道什么是異常表,那么我建議你讀一下這篇文章
看完這篇Exception 和 Error,和面試官扯皮就沒(méi)問(wèn)題了
synchronized 修飾方法的底層原理
方法的同步是隱式的,也就是說(shuō) synchronized 修飾方法的底層無(wú)需使用字節(jié)碼來(lái)控制,真的是這樣嗎?我們來(lái)反編譯一波看看結(jié)果
public?class?SynchronizedTest?{
????private?int?i;
????public?synchronized?void?syncTask(){
????????i++;
????}
}
這次我們使用?javap -verbose?來(lái)輸出詳細(xì)的結(jié)果

從字節(jié)碼上可以看出,synchronized 修飾的方法并沒(méi)有使用 monitorenter 和 monitorexit 指令,取得代之是ACC_SYNCHRONIZED 標(biāo)識(shí),該標(biāo)識(shí)指明了此方法是一個(gè)同步方法,JVM 通過(guò)該 ACC_SYNCHRONIZED 訪問(wèn)標(biāo)志來(lái)辨別一個(gè)方法是否聲明為同步方法,從而執(zhí)行相應(yīng)的同步調(diào)用。這就是 synchronized 鎖在同步代碼塊上和同步方法上的實(shí)現(xiàn)差別。

0、重磅!兩萬(wàn)字長(zhǎng)文總結(jié),梳理 Java 入門(mén)進(jìn)階哪些事(推薦收藏)

