JUC并發(fā)編程之Synchronized關(guān)鍵字詳解

對了,后續(xù)我也會對ReenLock鎖進行一個源碼分析,大家伙可以敬請期待。
public class StaticTest01 {/*** 靜態(tài)方法加鎖,它的鎖就是加在 StaticTest01.class 上* 因為是靜態(tài)方法,所以是通過類進行調(diào)用的,那么鎖就加在類上面*/public synchronized static void decrStock() {System.out.println("上鎖");}}
public class StaticTest02 {/*** 非靜態(tài)方法加鎖,因為非靜態(tài),所以需要進行 new對象,然后才能使用該方法* 那么鎖就是加在 new 對象的這個對象上,例如:StaticTest02 syn = new StaticTest02();* 那么鎖就是加在 syn 這個對象上*/public synchronized void decrStock() {System.out.println("上鎖");}}
public class StaticTest03 {private static Object object = new Object();/*** 非靜態(tài)方法代碼塊加鎖,那么鎖就是加在 object 這個成員變量上* 可以針對一部分代碼塊,而非整個方法*/public void decrStock() {synchronized (object) {System.out.println("上鎖");}}}
需要注意的是:synchronized關(guān)鍵字被編譯成字節(jié)碼后會被翻譯成monitorenter 和 monitorexit 兩條指令分別在同步塊邏輯代碼的起始位置與結(jié)束位置。
synchronized在JVM里的實現(xiàn)都是 基于進入和退出Monitor對象來實現(xiàn)方法同步和代碼塊同步,雖然具體實現(xiàn)細節(jié)不一樣,但是都可以通過成對的MonitorEnter和MonitorExit指令來實現(xiàn)。
基于字節(jié)碼文件,來看看同步塊代碼與同步方法它們之間的區(qū)別
public class StaticTest03 {private static Object object = new Object();/*** 非靜態(tài)方法代碼塊加鎖,那么鎖就是加在 object 這個成員變量上* 只不過代碼塊,可以針對一部分代碼塊,而非整個方法*/public void decrStock() {synchronized (object) {System.out.println("上鎖");}}}

monitorexit,指令出現(xiàn)了兩次,第1次為同步正常退出釋放鎖;第2次為發(fā)生異步退出釋放鎖;
通過上面兩段描述,我們應(yīng)該能很清楚的看出Synchronized的實現(xiàn)原理,Synchronized的語義底層是通過一個monitor的對象來完成,其實wait/notify等方法也依賴于monitor對象,這就是為什么只有在同步的塊或者方法中才能調(diào)用wait/notify等方法,否則會拋出java.lang.IllegalMonitorStateException的異常的原因。
接著來看同步方法:
public class StaticTest02 {/*** 非靜態(tài)方法加鎖,因為非靜態(tài),所以需要進行 new對象,然后才能使用該方法* 那么鎖就是加在 new 對象的 這個 對象上,例如:StaticTest02 syn = new StaticTest02();* 那么鎖就是加在 syn 這個對象上*/public synchronized void decrStock() {System.out.println("上鎖");}}

當(dāng)方法調(diào)用時,調(diào)用指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標(biāo)志是否被設(shè)置,如果設(shè)置了,執(zhí)行線程將先獲取monitor,獲取成功之后才能執(zhí)行方法體,方法執(zhí)行完后再釋放monitor。在方法執(zhí)行期間,其他任何線程都無法再獲得同一個monitor對象。
兩種同步方式本質(zhì)上沒有區(qū)別,只是方法的同步是一種隱式的方式來實現(xiàn),無需通過字節(jié)碼來完成。兩個指令的執(zhí)行是JVM通過調(diào)用操作系統(tǒng)的互斥原語mutex來實現(xiàn),被阻塞的線程會被掛起、等待重新調(diào)度,會導(dǎo)致“用戶態(tài)和內(nèi)核態(tài)”兩個態(tài)之間來回切換,對性能有較大影響。
我們前面也有說到,同步鎖它是加在對象上的,那他在對象里面是如何存儲的呢?來分析分析
在HotSpot虛擬機中,對象在內(nèi)存中存儲的布局可以分為三塊區(qū)域:對象頭(Header)、實例數(shù)據(jù)(Instance Data)和對齊填充(Padding)。
但看這段文字估計還有點懵,我在這里放上一張圖片。

前面也說到了對象的組成部分,結(jié)合上圖進行分析,在HotSpot虛擬機的對象頭包括兩部分信息,第一部分是“Mark Word”,用于存儲對象自身的運行時數(shù)據(jù), 如哈希碼(HashCode)、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程ID、偏向時間戳等等,它是實現(xiàn)輕量級鎖和偏向鎖的關(guān)鍵,這部分數(shù)據(jù)的長度在32位和64位的虛擬機(暫不考慮開啟壓縮指針的場景)中分別為32個和64個Bits,為了節(jié)省內(nèi)存,如果我們機器是64位系統(tǒng),則jvm會自動開啟指針壓縮,將它壓縮成32位,所以本文就基于32位來進行分析。
再繼續(xù)放上,基于32位虛擬機的一個對象頭表格
注意:對象頭信息是與對象自身定義的數(shù)據(jù)無關(guān)的額外存儲成本,但是考慮到虛擬機的空間效率,Mark Word被設(shè)計成一個非固定的數(shù)據(jù)結(jié)構(gòu)以便在極小的空間內(nèi)存存儲盡量多的數(shù)據(jù),它會根據(jù)對象的狀態(tài)復(fù)用自己的存儲空間,也就是說,Mark Word會隨著程序的運行發(fā)生變化
說到這,前面我有說到j(luò)vm它默認開啟了指針壓縮,其實我們也可以手動將其關(guān)閉,主要看場景決定吧
手動設(shè)置-XX:+UseCompressedOops有了以上內(nèi)容的鋪墊,我們就可以來聊一聊偏向鎖、輕量級鎖、自旋鎖,它們是什么東西,然后再來分析它們在對象頭中產(chǎn)生了什么樣的差異。
輕量級鎖
自旋鎖
鎖的對象頭分析:
<dependency><groupId>org.openjdk.jol</groupId><artifactId>jol-core</artifactId><version>0.10</version></dependency>
public class StaticTest04 {public static void main(String[] args) {Object o = new Object();System.out.println(ClassLayout.parseInstance(o).toPrintable());}}

以上圖為例,將信息頭反過來后,我們結(jié)合上面的表格查看,在最后的兩位是 "01" ,是01則就是無所狀態(tài)的標(biāo)識
//對象頭信息00000001 00000000 00000000 00000000//將信息返回來后00000000 00000000 00000000 00000001
public class StaticTest04 {public static void main(String[] args) {Object o = new Object();System.out.println(ClassLayout.parseInstance(o).toPrintable());synchronized (o) {System.out.println(ClassLayout.parseInstance(o).toPrintable());}}}

//加同步塊后對象頭信息01001000 11110010 11001110 00000010//倒序轉(zhuǎn)換后的對象頭信息00000010 11001110 11110010 01001000
public class StaticTest04 {public static void main(String[] args) throws InterruptedException {TimeUnit.SECONDS.sleep(5);Object o = new Object();System.out.println(ClassLayout.parseInstance(o).toPrintable());synchronized (o) {System.out.println(ClassLayout.parseInstance(o).toPrintable());}}}

//加同步塊后對象頭信息00000101 01010000 01101110 00000011//倒序轉(zhuǎn)換后的對象頭信息00000011 01101110 01010000 00000101
@Slf4jpublic class StaticTest05 {public static void main(String[] args) throws InterruptedException {TimeUnit.SECONDS.sleep(5);Object o = new Object();log.info(ClassLayout.parseInstance(o).toPrintable());new Thread(() -> {synchronized (o){log.info(ClassLayout.parseInstance(o).toPrintable());}}).start();TimeUnit.SECONDS.sleep(2);new Thread(() -> {synchronized (o){log.info(ClassLayout.parseInstance(o).toPrintable());}}).start();}}

最后再來看看,如何晉升成的重量級鎖的,先看代碼
@Slf4jpublic class StaticTest06 {public static void main(String[] args) throws InterruptedException {TimeUnit.SECONDS.sleep(5);Object o = new Object();Thread threadA = new Thread(() -> {synchronized (o) {log.info(ClassLayout.parseInstance(o).toPrintable());try {//讓線程晚點死亡TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}}});Thread threadB = new Thread(() -> {synchronized (o) {log.info(ClassLayout.parseInstance(o).toPrintable());try {//讓線程晚點死亡TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}}});//兩個線程同時啟動,模擬并發(fā)同時請求threadA.start();threadB.start();}}
運行結(jié)果圖,從圖中我們看到,我們的對象頭中的鎖是不是已經(jīng)成了重量級鎖了,那么再來看看這段代碼它是怎么模擬的,首先我們需要知道重量級鎖它是在鎖競爭非常激烈的時候才成為的,這段代碼模擬的是,我啟動兩個線程,第一個線程對對象進行加鎖,然后睡眠兩秒,模擬程序在處理業(yè)務(wù),然后第二個線程一直在等待第一個線程釋放鎖,在等待的過程中,會觸發(fā)自旋鎖,如果自旋鎖達到了閾值,則會直接讓第二個線程進行阻塞,從而線程2晉升為重量級鎖

當(dāng)然synchronized在1.6版本優(yōu)化中還加了兩個細節(jié)點的優(yōu)化,例如鎖粗化、鎖消除這兩個點。
鎖粗化:
例如以下這段代碼的極端情況
public class Test06 {public static void main(String[] args) {Object o = new Object();synchronized (o) {//業(yè)務(wù)邏輯處理System.out.println("鎖粗化1");}synchronized (o) {//業(yè)務(wù)邏輯處理System.out.println("鎖粗化2");}synchronized (o) {//業(yè)務(wù)邏輯處理System.out.println("鎖粗化3");}}}
上面的代碼是有三塊需要同步操作的,但在這三塊需要同步操作的代碼之間,需要做業(yè)務(wù)邏輯的工作,而這些工作只會花費很少的時間,那么我們就可以把這些工作代碼放入鎖內(nèi),將三個同步代碼塊合并成一個,以降低多次鎖請求、同步、釋放帶來的系統(tǒng)性能消耗,合并后的代碼如下:
public class Test06 {public static void main(String[] args) {Object o = new Object();synchronized (o) {//業(yè)務(wù)邏輯處理System.out.println("鎖粗化1");//業(yè)務(wù)邏輯處理System.out.println("鎖粗化2");//業(yè)務(wù)邏輯處理System.out.println("鎖粗化3");}}}
public class Test07 {public static void main(String[] args) {method();}public static void method() {Object o = new Object();synchronized (o) {System.out.println("鎖消除");}}}
分析上面這段代碼,前面說到過鎖消除的依據(jù)是逃逸分析,當(dāng)線程在調(diào)用我們的方法的時候,會對該方法進行逃逸分析,發(fā)現(xiàn)該方法里的對象不會被其他線程所共享,那么它會認為在里面進行加synchronized沒有任何用處,所以最后會底層會將進行優(yōu)化,將synchronized進行刪除。那么這就是鎖消除啦。
我是黎明大大,我知道我沒有驚世的才華,也沒有超于凡人的能力,但畢竟我還有一個不屈服,敢于選擇向命運沖鋒的靈魂,和一個就是傷痕累累也要義無反顧走下去的心。
如果您覺得本文對您有幫助,還請關(guān)注點贊一波,后期將不間斷更新更多技術(shù)文章

●JUC并發(fā)編程之MESI緩存一致協(xié)議詳解
●JUC并發(fā)編程之Volatile關(guān)鍵字詳解

