java程序中我們可以使用synchronized關(guān)鍵字對程序加鎖,它可以保證方法或者代碼塊運(yùn)行時同一時刻只有一個方法可以進(jìn)入到臨界區(qū)域,同時它還可以保證共享變量的內(nèi)存可見性,synchronzied關(guān)鍵字可以聲明一個同步代碼塊,也可以用來修飾靜態(tài)方法或者實例方法。當(dāng)synchronized修飾在不同地方時,它也是在對不同對象進(jìn)行加鎖操作:
1.同步代碼塊:鎖是括號里面的對象
2.靜態(tài)方法:瑣是當(dāng)前類的class對象
3.實例方法:鎖是當(dāng)前實例對象
當(dāng)聲明synchronized代碼塊時,編譯而成的字節(jié)碼將會包含monitorenter和monitorexit兩個指令,這兩種指令都會消耗操作數(shù)棧上的一個引用類型的元素(代碼塊中小括號里面的對象)作為加鎖和解鎖的對象。
public void foo(Object sync) {
synchronized (sync) {
sync.hashCode();
}
}
// 上面的 Java 代碼將編譯為下面的字節(jié)碼
public void foo(java.lang.Object);
Code:
0: aload_1
1: dup
2: astore_2
3: monitorenter
4: aload_1
5: invokevirtual java/lang/Object.hashCode:()I
8: pop
9: aload_2
10: monitorexit
11: goto 19
14: astore_3
15: aload_2
16: monitorexit
17: aload_3
18: athrow
19: return
Exception table:
from to target type
4 11 14 any
14 17 14 any
從上面的字節(jié)碼中可以看到有一個monitorenter指令和多個monitorexit指令,這是因為要確保在任何情況下退出都會釋放掉鎖。關(guān)于monitorenter和monitorexit的作用,我們可以抽象的理解為每個鎖對象都擁有一個鎖的計數(shù)器和一個持有該鎖的線程指針。
當(dāng)執(zhí)行到monitorenter時如果鎖計數(shù)器個數(shù)為零則代表沒有其他線程鎖定,這時java虛擬機(jī)會將鎖對象的持有線程設(shè)置為當(dāng)前線程,并把計數(shù)器設(shè)置為1。當(dāng)鎖計數(shù)器不為零時判斷持有鎖對象的線程是不是當(dāng)前線程,如果是當(dāng)前線程則把計數(shù)器加1。(因為synchronized是可重入鎖)當(dāng)執(zhí)行到monitorexit時,jvm會將計數(shù)器個數(shù)減1。當(dāng)計數(shù)器等于零的時候代表鎖已經(jīng)釋放。
當(dāng)synchronized標(biāo)記方法時,在字節(jié)碼中flags包括ACC_SYNCHRONIZED。此標(biāo)記標(biāo)識進(jìn)入該方法時java虛擬機(jī)要進(jìn)行monitorenter操作,在退出時進(jìn)行(正常退出或者異常退出)monitorexit操作。
二 、JAVA對象頭
在java虛擬機(jī)中,每個java對象都有一個對象頭(object header),由標(biāo)記字段(Mark Word)和類型指針(Klass Pointer)構(gòu)成。標(biāo)記字段用來存儲對象運(yùn)行時Java虛擬機(jī)有關(guān)該對象的運(yùn)行數(shù)據(jù),如哈希碼、GC信息、鎖狀態(tài)標(biāo)志、線程持有的鎖等等。類型指針是對象指向它的類元數(shù)據(jù)的指針,Java虛擬機(jī)通過該指針確定這個對象是什么類的實例。
三、JVM鎖優(yōu)化
重量級鎖
重量級鎖是java虛擬機(jī)中最基本的鎖的實現(xiàn)方式,在這種鎖狀態(tài)下,獲取鎖失敗的線程會進(jìn)入阻塞狀態(tài),當(dāng)目標(biāo)鎖被釋放時喚醒阻塞線程。
java線程中的阻塞和喚醒都是依靠操作系統(tǒng)來實現(xiàn)的,但是這種方式會有系統(tǒng)調(diào)用,需要從操作系統(tǒng)的用戶狀態(tài)切換到內(nèi)核狀態(tài),開銷很大。