我們都知道以舒,多個(gè)線程并發(fā)訪問共享變量或者共享資源就回來帶來線程安全問題毒涧。
于是就可以想到一種保障線程安全的方法--將多個(gè)線程的并發(fā)訪問轉(zhuǎn)換位串行訪問,即一個(gè)共享數(shù)據(jù)一次只能被一個(gè)線程訪問奠旺。這個(gè)就是鎖了舌缤。
java平臺(tái)的鎖包括內(nèi)部鎖和顯式鎖。
內(nèi)部鎖是通過synchronized關(guān)鍵字實(shí)現(xiàn)的黔帕,顯式鎖是通過Lock接口的實(shí)現(xiàn)類實(shí)現(xiàn)的。
鎖的作用:保障原子性蹈丸,可見性成黄,和有序性。
- 鎖是通過互斥保障原子性的逻杖,因?yàn)殒i一次只能被一個(gè)線程持有奋岁,就保證了臨界區(qū)代碼一次只能被一個(gè)線程執(zhí)行,這使得臨界區(qū)代碼所執(zhí)行的操作具有不可分割的特性荸百,即具備原子性闻伶。
- 鎖的獲得隱含著刷新處理器緩存這個(gè)動(dòng)作,即執(zhí)行臨界區(qū)代碼前够话,可以將寫線程對共享變量所做的更新同步到該線程執(zhí)行處理器的高速緩存中蓝翰。而鎖的釋放隱含著沖刷處理器緩存這個(gè)動(dòng)作。使得寫線程對共享變量所作的更新能夠被推送到該線程執(zhí)行處理器的高速緩存中女嘲,從而對讀線程可同步畜份。因此,鎖能夠保證可見性欣尼。
- 由于鎖對可見性的保證爆雹,寫線程在臨界區(qū)中對任何一個(gè)共享變量所做的更新都對線程可見。由于臨界區(qū)內(nèi)的操作具有原子性,因此寫線程對共享變量的更新同時(shí)對讀線程可見
synchronized關(guān)鍵字
- synchronized可以用來修飾方法或者代碼塊
- 作為鎖句柄的變量通常用final修飾钙态,這是因?yàn)殒i句柄變量的值一旦改變慧起,會(huì)導(dǎo)致執(zhí)行同一個(gè)同步快的多個(gè)線程實(shí)際上使用不同的鎖,從而導(dǎo)致競態(tài)册倒。
- 線程對內(nèi)部鎖的申請與釋放的動(dòng)作由java虛擬機(jī)負(fù)責(zé)代為實(shí)施蚓挤。
內(nèi)部鎖的調(diào)度:java虛擬機(jī)為每個(gè)內(nèi)部鎖分配一個(gè)入口集,用于記錄等待獲得相應(yīng)內(nèi)部鎖的線程剩失。多個(gè)線程申請同一個(gè)鎖的時(shí)候屈尼,只有一個(gè)申請者能申請成為該鎖的持有線程,而其他申請者的申請操作會(huì)失敗拴孤。申請失敗的線程會(huì)被暫停并被存入相應(yīng)鎖的入口集中等待再次申請鎖的機(jī)會(huì)脾歧。當(dāng)線程申請的鎖被釋放時(shí),該鎖的入口集中的一個(gè)任意線程會(huì)被java虛擬機(jī)喚醒演熟,從而得到再次申請鎖的機(jī)會(huì)鞭执。
-
原理
1.理解Java對象頭與Monitor
在JVM中,對象在內(nèi)存中的布局分為三塊區(qū)域:對象頭芒粹、實(shí)例數(shù)據(jù)和對齊填充兄纺,如圖:
image.png
實(shí)例變量:存放類的屬性數(shù)據(jù)信息,包括父類的屬性信息化漆,如果是數(shù)組的實(shí)例部分還包括數(shù)組的長度估脆,這部分內(nèi)存按4字節(jié)對齊。
對齊填充:由于虛擬機(jī)要求對象得起始地址必須是8字節(jié)的整數(shù)倍座云。填充數(shù)據(jù)不是必須存在的疙赠,僅僅是為了字節(jié)對齊。
對象頭:包含Mark word和class metadata address兩部分朦拖。
mark work的存儲(chǔ)內(nèi)容如下
image.png
metadata address:即元數(shù)據(jù)的指針圃阳,虛擬機(jī)通過這個(gè)指針來確定這個(gè)對象是哪個(gè)類的實(shí)例。
如果對象是數(shù)組璧帝,那對象頭中還必須有一塊用于記錄數(shù)組長度的數(shù)據(jù)捍岳,因?yàn)樘摂M機(jī)可以通過普通java對象的元數(shù)據(jù)信息確定java對象的大小,但是從數(shù)組的元數(shù)據(jù)中無法確定數(shù)組的大小睬隶。
mark work里面的重量級鎖锣夹,也就是我們通常說的synchronized的對象鎖,鎖標(biāo)識(shí)位為10理疙,其中指針指向的是monitor對象(也稱為管程或者監(jiān)視器鎖)的起始地址晕城。每個(gè)對象都存在一個(gè)monitor與之關(guān)聯(lián),monitor可以與對象一起創(chuàng)建銷毀窖贤,也可以當(dāng)線程試圖獲取對象鎖時(shí)自動(dòng)生成砖顷,當(dāng)一個(gè)monitor被某個(gè)線程持有后贰锁,它便處于鎖定狀態(tài)。
monitor的實(shí)現(xiàn)
monitor其實(shí)是一種同步工具滤蝠,被描述為一個(gè)對象豌熄,他的義務(wù)是保證只有一個(gè)線程可以訪問被保護(hù)的數(shù)據(jù)和代碼。
在HotSpot中物咳,monitor是基于c++實(shí)現(xiàn)的锣险,由ObjectMonitor實(shí)現(xiàn),結(jié)構(gòu)如下
ObjectMonitor() {
_header = NULL;
_count = 0;//用來記錄該線程獲取鎖的次數(shù)
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;//指向持有ObjectMonitor對象的線程
_WaitSet = NULL;//存放處于wait狀態(tài)的線程隊(duì)列
_WaitSetLock = 0 ;
_Responsible = NULL ;//鎖的重入次數(shù)
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ;//存放處于等待鎖block狀態(tài)的線程隊(duì)列
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
當(dāng)多個(gè)線程同時(shí)訪問一段同步代碼的時(shí)候览闰,首先會(huì)進(jìn)入_EntryList隊(duì)列中芯肤,當(dāng)某個(gè)線程獲取到對象的monitor后進(jìn)入_Owner區(qū)域,并把monitor中的_owner變量設(shè)置為當(dāng)前線程压鉴,同時(shí)monitor中的計(jì)數(shù)器_count加1崖咨,即獲得對象鎖
若持有monitor的線程調(diào)用wait()方法,將釋放當(dāng)前持有的monitor,_owner變量恢復(fù)為null,_count自減1油吭,同時(shí)該線程進(jìn)入_WaitSet集合中等待被喚醒击蹲。若當(dāng)前線程執(zhí)行完畢也將釋放monitor并復(fù)位變量的值,以便其他線程進(jìn)入獲取monitor.
以下是獲取鎖的過程:
monitor對象存在于每個(gè)java對象的對象頭中婉宰,synchronized鎖便是通過這種方式獲取鎖的歌豺,這也是為什么java中任意對象可以作為鎖的原因。
同時(shí)也是notify/notifyAll/wait等方法存在于頂級對象object中的原因心包,在使用這幾個(gè)方法時(shí)类咧,必須處于synchronized代碼塊或者synchronized方法中,否則就會(huì)拋出IllegalMonitorStateException異常蟹腾,因?yàn)檎{(diào)用這幾個(gè)方法前必須拿到當(dāng)前對象的監(jiān)視器monitor對象轮听,也就是說notify/notifyAll/wait這幾個(gè)方法依賴于monitor對象,而要獲取monitor,就必須通過synchronized關(guān)鍵字岭佳,這也就是notify/notifyAll/wait方法必須在synchronized代碼塊或者synchronized方法調(diào)用的原因了。
synchronized的實(shí)現(xiàn)原理
public class SynchronizedDemo {
//同步方法
public synchronized void doSth(){
System.out.println("Hello World");
}
//同步代碼塊
public void doSth1(){
synchronized (SynchronizedDemo.class){
System.out.println("Hello World");
}
}
}
看一下上面代碼的字節(jié)碼萧锉,如下
public synchronized void doSth();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello World
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
public void doSth1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: ldc #5 // class com/hollis/SynchronizedTest
2: dup
3: astore_1
4: monitorenter
5: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #3 // String Hello World
10: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: aload_1
14: monitorexit
15: goto 23
18: astore_2
19: aload_1
20: monitorexit
21: aload_2
22: athrow
23: return
通過反編譯后代碼可以看出珊随,對于同步方法罗捎,JVM采用ACC_SYNCHRONIZED標(biāo)記符來實(shí)現(xiàn)同步达皿。對于同步代碼塊,JVM采用monitorenter,monitorexit兩個(gè)指令來實(shí)現(xiàn)同步寸爆。
當(dāng)某個(gè)線程要訪問某個(gè)方法的時(shí)候禀崖,會(huì)檢查是否有ACC_SYNCHRONIZED衩辟,如果有設(shè)置,則需要先獲得監(jiān)視器波附,然后再執(zhí)行方法艺晴,方法執(zhí)行后釋放監(jiān)視器鎖昼钻。如果在方法執(zhí)行過程中,發(fā)生異常封寞,并且方法內(nèi)部沒有處理該異常然评,那么異常被拋到方法外面之前監(jiān)視器鎖會(huì)自動(dòng)釋放。
同步代碼塊使用monitorenter和monitorexit兩個(gè)指令實(shí)現(xiàn)狈究⊥胩剩可以把執(zhí)行monitorenter指令理解為加鎖,執(zhí)行monitorexit理解為釋放鎖抖锥。每個(gè)對象維護(hù)著一個(gè)記錄著被鎖次數(shù)得計(jì)數(shù)器亿眠。未被鎖定得對象得該計(jì)數(shù)器為0,當(dāng)一個(gè)線程獲得鎖后磅废,該計(jì)數(shù)器自增變?yōu)?纳像,當(dāng)一個(gè)線程再次獲得該對象得鎖時(shí),計(jì)數(shù)器再次自增还蹲。當(dāng)線程釋放鎖時(shí)爹耗,計(jì)數(shù)器自減。當(dāng)計(jì)數(shù)器為0的時(shí)候谜喊,鎖被釋放潭兽。
synchronized為什么被叫重量級鎖?
java的線程是映射到操作系統(tǒng)原生線程之上的斗遏,如果要阻塞或喚醒一個(gè)線程就需要操作系統(tǒng)的幫忙山卦,這就要從用戶態(tài)轉(zhuǎn)換到核心態(tài),因此狀態(tài)轉(zhuǎn)換需要花費(fèi)很多的處理器時(shí)間诵次,對于代碼簡單的同步快狀態(tài)轉(zhuǎn)換消耗的時(shí)間有可能比用戶代碼執(zhí)行的時(shí)間還要長账蓉,所以說是一個(gè)重量級的操作。