1 synchronized
1.1 synchronized介紹
- synchronized機(jī)制提供了對每個(gè)對象相關(guān)的隱式監(jiān)視器鎖,并強(qiáng)制所有鎖的獲取和釋放都必須在同一個(gè)塊結(jié)構(gòu)中。當(dāng)獲取了多個(gè)鎖時(shí),必須以相反的順序釋放。即synchronized對于鎖的釋放是隱式的。
- synchronized同步塊對于同一條線程是可重入的,不會(huì)出現(xiàn)把自己鎖死的問題。
- synchronized可以修飾類、方法(包括靜態(tài)方法)、代碼塊。修飾類和靜態(tài)方法時(shí),鎖的對象是Class對象;修飾普通方法時(shí),鎖的是調(diào)用該方法的對象;修飾代碼塊時(shí),鎖的是方法塊括號(hào)里的對象。
- synchronized性能中避免“讀/讀”操作,但讀操作頻繁,通過ReentrantLock提供的ReadWriteLock讀寫鎖來解決該問題;阻塞線程時(shí),需要OS不斷的從用戶態(tài)轉(zhuǎn)到核心態(tài),消耗處理器時(shí)間,通過自適應(yīng)自旋來解決該問題。
- synchronized的鎖是存放在Java對象頭里的。
1.2 synchronized的三種使用場景
1.2.1 修飾實(shí)例方法
1)說明:對當(dāng)前對象實(shí)例this加鎖
2)示例:
public class Demo1 {
public synchronized void methodA() {
System.out.println("synchronized 修飾 普通實(shí)例方法");
}
}
1.2.2 修飾靜態(tài)方法
1)對當(dāng)前類的Class對象加鎖
2)示例
public class Demo2 {
public synchronized static void methodB() {
System.out.println("synchronized 修飾 靜態(tài)方法");
}
}
1.2.3 修飾代碼塊
1)給指定對象加鎖
2)說明
public class Demo3 {
public void methodC() {
synchronized(this) {
System.out.println("synchronized代碼塊對當(dāng)前對象加鎖");
}
}
public void methodC() {
synchronized(Demo3.Class) {
System.out.println("synchronized代碼塊對當(dāng)前類的Class對象加鎖");
}
}
}
1.3 synchronized實(shí)現(xiàn)同步的基礎(chǔ)
Java中每一個(gè)對象都可以作為鎖,這是synchronized實(shí)現(xiàn)同步的基礎(chǔ):
- 普通同步方法:鎖是當(dāng)前實(shí)例對象;
- 靜態(tài)同步方法:鎖是當(dāng)前類的Class對象;
- 同步方法塊:鎖是括號(hào)里面的對象;
同步代碼塊:monitorenter指令插入到同步代碼塊的開始位置,monitorexit指令插入到同步代碼塊的結(jié)束位置,JVM需要保證每一個(gè)monitorexit和monitorenter相對應(yīng),任何對象都有一個(gè)monitor與之相對應(yīng),任何對象都有一個(gè)monitor與之相關(guān)聯(lián),當(dāng)且一個(gè)monitor被持有之后,他處于鎖定狀態(tài),線程執(zhí)行到monitorenter指令時(shí),將會(huì)嘗試獲取對象所對應(yīng)的monitor所有權(quán),即嘗試獲取對象的鎖。
同步方法:synchronized方法則會(huì)被翻譯成普通的方法調(diào)用和返回指令如:invokevirtual、areturn指令,在VM字節(jié)碼層面并沒有任何特別的指令來實(shí)現(xiàn)被synchronized修飾的方法,而是在Class文件的方法表中將該方法的access_flags字段中的synchronized標(biāo)志位置1,表示該方法是同步方法并使用調(diào)用該方法的對象或該方法所屬的Class在JVM的內(nèi)部對象表示Klass做為鎖對象。
1.4 synchronized反編譯字節(jié)碼
synchronized字節(jié)碼層面實(shí)現(xiàn)的鎖(鎖計(jì)數(shù)器)。
- synchronized關(guān)鍵字經(jīng)編譯后,在同步塊前后分別形成monitorenter和monitorexit兩個(gè)字節(jié)碼指令;
- 在執(zhí)行monitorenter指令時(shí),首先要嘗試獲取對象的鎖,若這個(gè)對象沒被鎖定,或當(dāng)前線程已經(jīng)擁有了那個(gè)對象的鎖,就把鎖的計(jì)數(shù)器加1;
- 在執(zhí)行monitorexit指令時(shí)會(huì)將鎖計(jì)數(shù)器減1,當(dāng)計(jì)數(shù)器為0時(shí),鎖就會(huì)被釋放;
- 若獲取對象鎖失敗,當(dāng)前線程進(jìn)入阻塞等待,直到對象鎖被另外一個(gè)線程釋放為止。
1.4.1 monitorenter
(重入鎖的體現(xiàn))每個(gè)對象都有一個(gè)監(jiān)視器鎖(monitor),當(dāng)monitor被占用時(shí)就會(huì)處于鎖定狀態(tài),線程執(zhí)行monitorenter指令時(shí)嘗試獲取monitor的所有權(quán)。
- 如果monitor的進(jìn)入數(shù)為0,則該線程進(jìn)入monitor,然后將進(jìn)入數(shù)設(shè)置為1,該線程即為monitor的所有者。
- 如果線程已經(jīng)占有該monitor,只是重新進(jìn)入,則進(jìn)入monitor的進(jìn)入數(shù)加1。
- 如果其他線程已經(jīng)占用了monitor,則該線程進(jìn)入阻塞狀態(tài),直至monitor的進(jìn)入數(shù)為0,再重新嘗試獲取monitor的所有權(quán)。
1.4.2 monitorexit
- 執(zhí)行monitorexit的線程必須是object所對應(yīng)的monitor的所有者。
- 指令執(zhí)行時(shí),monitor的進(jìn)入數(shù)減1,如果減1后進(jìn)入數(shù)為0,那么線程就會(huì)退出monitor,不再是這個(gè)monitor的所有者。其他被這個(gè)monitor阻塞的線程可以嘗試去獲取這個(gè)monitor的所有權(quán)。
1.5 Java對象
1.5.1 Java對象組成
??Java對象存儲(chǔ)在堆內(nèi)存中,對象由對象頭、實(shí)例變量和填充字節(jié)組成。
1.5.2 對象頭
- synchronized用的鎖是存放在Java對象頭里。Hotspot虛擬機(jī)的對象頭主要包括兩部分?jǐn)?shù)據(jù):Mark Word(標(biāo)記字段)、Klass Pointer(類型指針)。
- Klass Point是對象指向它的類元數(shù)據(jù)的指針,虛擬機(jī)通過這個(gè)指針來確定這個(gè)對象是哪個(gè)類的實(shí)例。
- Mark Word用于存儲(chǔ)對象自身的運(yùn)行時(shí)數(shù)據(jù),它是實(shí)現(xiàn)輕量級(jí)鎖和偏向鎖的關(guān)鍵。如哈希碼(HashCode)、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程 ID、偏向時(shí)間戳等等。Java對象頭一般占有兩個(gè)機(jī)器碼(在32位虛擬機(jī)中,1個(gè)機(jī)器碼等于4字節(jié),也就是32bit),但是如果對象是數(shù)組類型,則需要三個(gè)機(jī)器碼,因?yàn)镴VM虛擬機(jī)可以通過Java對象的元數(shù)據(jù)信息確定Java對象的大小,但是無法從數(shù)組的元數(shù)據(jù)來確認(rèn)數(shù)組的大小,所以用一塊來記錄數(shù)組長度。
下圖是Java對象頭的存儲(chǔ)結(jié)構(gòu)(32位虛擬機(jī)):
| 對象的hashCode | 對象的分代年齡 | 是否為偏向鎖 | 鎖標(biāo)志位 |
|---|---|---|---|
| 25bit | 4bit | 1bit | 2bit |
對象頭信息是與對象自身定義的數(shù)據(jù)無關(guān)的額外存儲(chǔ)成本,單考慮VM的空間小了,Mark Word設(shè)計(jì)成一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu)以便在極小的空間內(nèi)存存儲(chǔ)盡量多的數(shù)據(jù),會(huì)根據(jù)對象的狀態(tài)復(fù)用自己的存儲(chǔ)空間,即Mark Word是隨著程序的運(yùn)行發(fā)生變化。
1.5.3 實(shí)例數(shù)據(jù)
是對象存儲(chǔ)的真正有效信息,存儲(chǔ)著自身定義的和從父類繼承下來的實(shí)例字段,字段的存儲(chǔ)順序會(huì)受到虛擬機(jī)的分配策略和字段在Java源碼中定義順序的影響,這部分內(nèi)存按4字節(jié)對齊。
1.5.4 對齊填充字段
由于虛擬機(jī)要求對象起始地址必須是8字節(jié)的整數(shù)倍。填充數(shù)據(jù)不是必須存在的,僅僅是為了字節(jié)對齊。
2 ReentrantLock
2.1 ReentrantLock介紹
- Lock提供一種無條件的、可輪詢的、定時(shí)的以及可中斷的鎖獲取操作,所有加鎖和解鎖都是顯式的。
- ReentrantLock實(shí)現(xiàn)Lock接口,并提供與synchronized相同的互斥性和內(nèi)存可見性。
- ReentrantLock提供和synchronized一樣的可重入的加鎖語義。
- ReentrantLock是顯式鎖,需要顯式進(jìn)行lock以及unlock操作。形式比內(nèi)置鎖復(fù)雜,必須在finally塊中釋放鎖,否則如果在被保護(hù)的代碼中拋出異常,這個(gè)鎖就永遠(yuǎn)都無法釋放。加鎖時(shí),需要考慮在try塊中拋出異常的情況,如果可能使對象處于某種不一致的狀態(tài),則需要更多的try-catch或try-finally代碼塊。
2.2 ReentrantLock示例
Lock lock = new ReentrantLock();
...
lock.lock();
try{
//更新對象狀態(tài)
//捕獲異常,并在必要時(shí)恢復(fù)不變性條件
}finally{
lock.unlock();
}
2.3 使用ReentrantLock的靈活性
(也是與synchronized的區(qū)別)
2.3.1 等待可中斷
??使用lock.lockInterruptibly()可以使得線程在等待鎖支持響應(yīng)中斷;
??使用lock.tryLock()可以使線程在等待一段時(shí)間過后如果還未獲得鎖就停止等待而非一直等待,更好的避免饑餓和死鎖問題;
2.3.2 公平鎖
??默認(rèn)情況下是非公平鎖,但是也可以是公平鎖,公平鎖就是鎖的等待隊(duì)列的FIFO,不建議使用,會(huì)浪費(fèi)許多時(shí)鐘周期,達(dá)不到最大利用率。
2.3.3 鎖可綁定多個(gè)條件
??與ReentrantLock搭配的通信方式是Condition,且可以為多個(gè)線程建立不同的Codition。
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
condition.await(); //等價(jià)于synchronized中的wait()方法
condition.signal(); //等價(jià)于notify()
condition.signalAll(); //等價(jià)于notifyAll()
2.4 讀-寫鎖
2.4.1 讀寫鎖的出現(xiàn)原因
??ReentrantLock實(shí)現(xiàn)一種標(biāo)準(zhǔn)的互斥鎖,每次最多只有一個(gè)線程能持有ReentrantLock,限制了并發(fā)性,互斥是一種保守的加鎖策略,雖然避免了“寫/寫”沖突和“寫/讀”沖突,但也避免了“讀/讀”沖突,而大部分情況下讀操作比較多,如果此時(shí)能夠放寬加鎖需求,允許多個(gè)讀操作的線程同時(shí)訪問數(shù)據(jù)結(jié)構(gòu),可以提升程序的性能(只要每個(gè)線程保證讀取到最新的數(shù)據(jù),并且在讀取數(shù)據(jù)時(shí)不會(huì)有其他線程修改數(shù)據(jù)就行)
2.4.2 ReentrantLock提供的非互斥的讀寫鎖的定義
??一個(gè)資源可以被多個(gè)讀操作訪問,或者被一個(gè)寫操作訪問,但兩者不能讀寫操作同時(shí)進(jìn)行。(多讀、一寫、不可同讀寫)
??讀-寫鎖是一種性能優(yōu)化措施,可以實(shí)現(xiàn)更高的并發(fā)性,提高程序的性能。
??當(dāng)鎖的持有時(shí)間較長并且大部分操作都不會(huì)修改被守護(hù)的資源時(shí),讀-寫鎖可以提高并發(fā)性。
3 Lock接口
3.1 Lock接口介紹
??Lock接口提供一種無條件的、可輪詢的、定時(shí)的以及可中斷的鎖獲取操作,所有加鎖和解鎖都是顯示的。
3.2 Lock方法
-
void lock();:獲取鎖。 -
void lockInterruptibly() throws InterruptedException;:若當(dāng)前線程未被中斷,獲取鎖。 -
boolean tryLock();:僅當(dāng)調(diào)用時(shí)鎖為空閑狀態(tài)才獲取鎖。 -
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;:如果鎖在給定的等待時(shí)間內(nèi)空閑,并且當(dāng)前線程未被中斷,則獲取鎖。 -
void unlock();:釋放鎖。 -
Condition newCondition();:返回綁定在此Lock實(shí)例的新Condition實(shí)例。
4 鎖優(yōu)化
4.1 jdk1.6對鎖進(jìn)行優(yōu)化:
??自旋鎖、適應(yīng)性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級(jí)鎖。
4.2 鎖的四個(gè)狀態(tài):
??無鎖狀態(tài)、偏向鎖狀態(tài)、輕量級(jí)鎖狀態(tài)、重量級(jí)鎖狀態(tài)。
??鎖可以升級(jí)(隨著鎖的競爭激烈程度而升級(jí)),但是不可降級(jí)。
4.3 自旋鎖
- 引入原因:線程的阻塞和喚醒需要CPU從用戶態(tài)轉(zhuǎn)為核心態(tài)以及核心態(tài)轉(zhuǎn)為用戶態(tài),頻繁的阻塞和喚醒對CPU來說是一件負(fù)擔(dān)很重的工作,勢必會(huì)給系統(tǒng)的并發(fā)性能帶來很大的壓力。同時(shí)我們發(fā)現(xiàn)在許多應(yīng)用上面,對象鎖的鎖狀態(tài)只會(huì)持續(xù)很短一段時(shí)間,為了這一段很短的時(shí)間頻繁地阻塞和喚醒線程是非常不值得的。
- 原理:就是讓該線程等待一段時(shí)間(執(zhí)行一段無意義的循環(huán)即可(自旋)),不會(huì)被立即掛起,看持有鎖的線程是否會(huì)很快釋放鎖。(線程自循環(huán)等待,不立即掛起)
- 缺點(diǎn):自旋不能代替阻塞,可以避免線程切換帶來的開銷,但是會(huì)占用處理器的時(shí)間,若持有鎖的線程很快釋放鎖,則自旋效率好;反之,自旋的線程會(huì)白白消耗掉處理器的資源,帶來性能上的浪費(fèi)。
- 針對缺點(diǎn)的解決方案:自旋等待時(shí)間有限度——超過定義的時(shí)間沒有獲得鎖就應(yīng)該被掛起。自旋鎖是JDK1.4.2引入,默認(rèn)關(guān)閉,使用-XX:UseSpining開啟,在JDK1.6默認(rèn)開啟,默認(rèn)自旋次數(shù)為10次,可以通過-XX:PreBlockSpin調(diào)整。最終是通過自適應(yīng)自旋鎖來解決這一問題。
4.4 自適應(yīng)自旋鎖
- 引入原因:自旋鎖的自旋次數(shù)是固定的,當(dāng)持有鎖不能很快釋放鎖時(shí),效率低下,浪費(fèi)處理器資源,自旋次數(shù)需要通過-XX:PreBlockSpin調(diào)整。所以引入自適應(yīng)自旋鎖不再固定自旋次數(shù)。
- 原理:由前一次在同一個(gè)鎖上的自旋時(shí)間及鎖的擁有者狀態(tài)決定。線程如果自旋成功了,那么下次自旋的次數(shù)會(huì)更加多,因?yàn)樘摂M機(jī)認(rèn)為既然上次成功了,那么此次自旋也很有可能會(huì)再次成功,那么它就會(huì)允許自旋等待持續(xù)的次數(shù)更多。反之,如果對于某個(gè)鎖,很少有自旋能夠成功的,那么在以后要或者這個(gè)鎖的時(shí)候自旋的次數(shù)會(huì)減少甚至省略掉自旋過程,以免浪費(fèi)處理器資源。(主要就是看前一次自旋成功與否,成功多就增加自旋次數(shù);成功少,就減少自旋次數(shù))
4.5 鎖消除
- 引入原因:在某些情況下,JVM檢測到不可能存在共享數(shù)據(jù)競爭,如果繼續(xù)加鎖,降低性能,無意義。
- 原理:鎖消除依據(jù)是逃逸分析的數(shù)據(jù)支持,檢測到不存在共享數(shù)據(jù)競爭,就消除鎖,從而節(jié)省請求鎖的時(shí)間。(逃逸分析檢測是否有共享數(shù)據(jù)競爭,若有,消除鎖)
4.6 鎖粗化
- 引入原因:一系列的連續(xù)加鎖解鎖操作,導(dǎo)致不必要的性能損耗。
- 原理:將多個(gè)連續(xù)的加鎖、解鎖操作連接在一起,擴(kuò)展成一個(gè)范圍更大的鎖。如Vector的add操作操作,JVM檢測到對同一個(gè)對象(vector)連續(xù)加鎖、解鎖操作,則合并成一個(gè)更大范圍的加鎖、解鎖操作,進(jìn)行鎖粗化。(多個(gè)連續(xù)加鎖、解鎖合并成一個(gè)更大范圍的鎖)
4.7 輕量級(jí)鎖(00)
- 引入原因:在沒有多線程競爭的前提下,減少傳統(tǒng)的重量級(jí)鎖使用操作系統(tǒng)互斥量產(chǎn)生的性能損耗。
- 原理:當(dāng)關(guān)閉偏向鎖功能或者多個(gè)線程競爭偏向鎖導(dǎo)致偏向鎖升級(jí)為輕量級(jí)鎖,則會(huì)嘗試獲取輕量級(jí)鎖,其步驟如下:
獲取鎖
- 1)判斷當(dāng)前對象是否處于無鎖狀態(tài)(hashcode、0、01),若是,則JVM首先將在當(dāng)前線程的棧幀中建立一個(gè)名為鎖記錄(Lock Record)的空間,用于存儲(chǔ)鎖對象目前的Mark Word的拷貝(官方把這份拷貝加了一個(gè)Displaced前綴,即Displaced Mark Word);否則執(zhí)行步驟 3);
- 2)JVM利用CAS操作嘗試將對象的Mark Word更新為指向Lock Record的指針,如果成功表示競爭到鎖,則將鎖標(biāo)志位變成00(表示此對象處于輕量級(jí)鎖狀態(tài)),執(zhí)行同步操作;如果失敗則執(zhí)行步驟 3);
- 3)判斷當(dāng)前對象的Mark Word是否指向當(dāng)前線程的棧幀,如果是則表示當(dāng)前線程已經(jīng)持有當(dāng)前對象的鎖,則直接執(zhí)行同步代碼塊;否則只能說明該鎖對象已經(jīng)被其他線程搶占了,這時(shí)輕量級(jí)鎖需要膨脹為重量級(jí)鎖,鎖標(biāo)志位變成10,后面等待的線程將會(huì)進(jìn)入阻塞狀態(tài);
釋放鎖:輕量級(jí)鎖的釋放也是通過CAS操作來進(jìn)行的,主要步驟如下:
- 1)取出在獲取輕量級(jí)鎖保存在Displaced Mark Word中的數(shù)據(jù);
- 2)用CAS操作將取出的數(shù)據(jù)替換當(dāng)前對象的Mark Word中,如果成功,則說明釋放鎖成功,否則執(zhí)行 3);
- 3)如果CAS操作替換失敗,說明有其他線程嘗試獲取該鎖,則需要在釋放鎖的同時(shí)需要喚醒被掛起的線程。
- 性能分析:對于輕量級(jí)鎖,其性能提升的依據(jù)是“對于絕大部分的鎖,在整個(gè)生命周期內(nèi)都是不會(huì)存在競爭的”,如果打破這個(gè)依據(jù)則除了互斥的開銷外,還有額外的CAS操作,因此在有多線程競爭的情況下,輕量級(jí)鎖比重量級(jí)鎖更慢;
4.8 偏向鎖(01)
- 引入原因:為了在無多線程競爭的情況下盡量減少不必要的輕量級(jí)鎖執(zhí)行路徑。輕量級(jí)鎖的加鎖解鎖操作是需要依賴多次CAS原子指令的。
- 原理
獲取鎖:
- 1)檢測Mark Word是否為可偏向狀態(tài),即是否為偏向鎖1,鎖標(biāo)識(shí)位為01;
- 2)若為可偏向狀態(tài),則測試線程ID是否為當(dāng)前線程ID,如果是,則執(zhí)行步驟 5),否則執(zhí)行步驟 3);
- 3)如果線程ID不為當(dāng)前線程ID,則通過CAS操作競爭鎖,競爭成功,則將Mark Word的線程ID替換為當(dāng)前線程ID,否則執(zhí)行步驟 4);
- 4)通過CAS競爭鎖失敗,證明當(dāng)前存在多線程競爭情況,當(dāng)?shù)竭_(dá)全局安全點(diǎn),獲得偏向鎖的線程被掛起,偏向鎖升級(jí)為輕量級(jí)鎖,然后被阻塞在安全點(diǎn)的線程繼續(xù)往下執(zhí)行同步代碼塊;
- 5)執(zhí)行同步代碼塊。
釋放鎖:偏向鎖的釋放采用了一種只有競爭才會(huì)釋放鎖的機(jī)制,線程是不會(huì)主動(dòng)去釋放偏向鎖,需要等待其他線程來競爭。偏向鎖的撤銷需要等待全局安全點(diǎn)(這個(gè)時(shí)間點(diǎn)是上沒有正在執(zhí)行的代碼)。其步驟如下:
- 1)暫停擁有偏向鎖的線程,判斷鎖對象是否還處于被鎖定狀態(tài);
- 2)撤銷偏向鎖,恢復(fù)到無鎖狀態(tài)(01)或者輕量級(jí)鎖的狀態(tài);
4.9 重量級(jí)鎖(10)
??重量級(jí)鎖通過對象內(nèi)部的監(jiān)視器(monitor)實(shí)現(xiàn),其中monitor的本質(zhì)是依賴于底層操作系統(tǒng)的Mutex Lock實(shí)現(xiàn),操作系統(tǒng)實(shí)現(xiàn)線程之間的切換需要從用戶態(tài)到內(nèi)核態(tài)的切換,切換成本非常高。
4.10 鎖的優(yōu)缺點(diǎn)的對比
| 鎖 | 優(yōu)點(diǎn) | 缺點(diǎn) | 適用場景 |
|---|---|---|---|
| 偏向鎖 | 加解鎖不需要額外操作,速度較快 若是發(fā)生線程間的鎖競爭,會(huì)產(chǎn)生鎖撤銷的消耗 | 適用于只有一個(gè)線程訪問同步塊的場景 | |
| 輕量級(jí)鎖 | 競爭的線程不會(huì)阻塞,程序響應(yīng)速度塊 | 不斷的自旋會(huì)消耗CPU資源 | 追求響應(yīng)時(shí)間,同步塊執(zhí)行速度很快 |
| 重量級(jí)鎖 | 線程競爭不使用自旋,CPU消耗少 | 線程會(huì)阻塞,相應(yīng)慢 | 追求吞吐量,同步塊執(zhí)行時(shí)間較長 |
Q&A
synchronized方法和synchronized塊的區(qū)別
- synchronized塊:是一種細(xì)粒度的并發(fā)控制,只會(huì)將塊中的代碼同步,位于方法內(nèi)、synchronized塊之外的代碼是可以被多個(gè)線程同時(shí)訪問到的,鎖的是方法塊后面括號(hào)里的對象;synchronized方法是一種粗粒度的并發(fā)控制,某一時(shí)刻,只能有一個(gè)線程執(zhí)行該synchronized方法,鎖的是調(diào)用該方法的對象。
- 同步代碼塊:monitorenter指令插入到同步代碼塊的開始位置,monitorexit指令插入到同步代碼塊的結(jié)束位置,JVM需要保證每一個(gè)monitorexit和monitorenter相對應(yīng),任何對象都有一個(gè)monitor與之相對應(yīng),任何對象都有一個(gè)monitor與之相關(guān)聯(lián),當(dāng)且一個(gè)monitor被持有之后,他處于鎖定狀態(tài),線程執(zhí)行到monitorenter指令時(shí),將會(huì)嘗試獲取對象所對應(yīng)的monitor所有權(quán),即嘗試獲取對象的鎖;同步方法:synchronized方法則會(huì)被翻譯成普通的方法調(diào)用和返回指令如:invokevirtual、areturn指令,在VM字節(jié)碼層面并沒有任何特別的指令來實(shí)現(xiàn)被synchronized修飾的方法,而是在Class文件的方法表中將該方法的access_flags字段中的synchronized標(biāo)志位置1,表示該方法是同步方法并使用調(diào)用該方法的對象或該方法所屬的Class在JVM的內(nèi)部對象表示Klass做為鎖對象。在常量池中添加了ACC_ SYNCHRONIZED標(biāo)識(shí)符,JVM就是根據(jù)該標(biāo)識(shí)符來實(shí)現(xiàn)方法的同步。
synchronized和ReentrantLock的異同
同:
- 都是可重入的;
- 都屬于同步互斥的手段;
異:
1 底層:synchronized是原生語法層面的互斥鎖;ReentrantLock是API層面的互斥鎖;
- 加鎖釋放鎖范式:synchronized是內(nèi)置鎖,獲取多個(gè)鎖后,以相反的順序隱式釋放鎖;ReentrantLock必須顯式加鎖釋放鎖,且可以自由的順序釋放鎖。
- 功能:ReentrantLock增加高級(jí)功能:等待可中斷、可實(shí)現(xiàn)公平鎖、鎖可以綁定多個(gè)條件。
| 功能 | 說明 |
|---|---|
| 等待可中斷 | 當(dāng)持有鎖的線程長期不釋放鎖的時(shí)候,正在等待的線程可以選擇放棄等待,改為處理其他事情,可中斷特性適用于處理執(zhí)行時(shí)間非常長的同步塊。 |
| 公平鎖 | ReentrantLock可以是公平鎖,默認(rèn)為非公平鎖,通過帶布爾值的構(gòu)造函數(shù)使用公平鎖,公平鎖是多個(gè)線程在等待同一個(gè)鎖時(shí),必須按照申請鎖的時(shí)間順序來一次獲得鎖;synchronized是非公平鎖,非公平鎖是不保證的,在鎖釋放時(shí),任何一個(gè)等待鎖的線程都有機(jī)會(huì)獲得鎖。 |
| 鎖綁定多個(gè)條件 | 指一個(gè)ReentrantLock對象可同時(shí)綁定多個(gè)Condition對象,多次調(diào)用newCondition()方法;而在synchronized中,鎖對象的wait()和notify()或notifyAll()方法可以實(shí)現(xiàn)一個(gè)隱含的條件,若要和多個(gè)條件關(guān)聯(lián),需要額外的添加一個(gè)鎖。 |
什么是可重入鎖?
??重入鎖實(shí)現(xiàn)重入性:每個(gè)鎖關(guān)聯(lián)一個(gè)線程持有者和計(jì)數(shù)器:
- 當(dāng)計(jì)數(shù)器為0時(shí)表示該鎖沒有被任何線程持有,那么任何線程都可能獲得該鎖而調(diào)用相應(yīng)的方法;
- 當(dāng)某一線程請求成功后,JVM會(huì)記下鎖的持有線程,并且將計(jì)數(shù)器置為1;
- 此時(shí)其它線程請求該鎖,則必須等待;而該持有鎖的線程如果再次請求這個(gè)鎖,就可以再次拿到這個(gè)鎖,同時(shí)計(jì)數(shù)器會(huì)遞增+1;
- 當(dāng)線程退出同步代碼塊時(shí),計(jì)數(shù)器會(huì)遞減-1,如果計(jì)數(shù)器為0,則釋放該鎖。
synchronized鎖的存儲(chǔ)位置?
- synchronized鎖存放的位置是Java對象頭里,如果對象是數(shù)組類型,則JVM用3個(gè)字節(jié)寬存儲(chǔ)對象頭,如果對象是非數(shù)組類型,則用2字節(jié)寬度存儲(chǔ)對象頭。
- 對象頭由mark word+類型指針組成。mark word默認(rèn)存儲(chǔ)對象的hashcode、分代年齡和鎖標(biāo)志位。
synchronized鎖存儲(chǔ)在對象頭,何為對象頭?
對象頭中包括兩部分?jǐn)?shù)據(jù):標(biāo)記字段和類型指針;
- 類型指針Klass Point是對象指向它的類元數(shù)據(jù)的指針,虛擬機(jī)通過這個(gè)指針來確定這個(gè)對象是哪個(gè)類的實(shí)例;
- 標(biāo)記字段Mark Word是一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu),用于存儲(chǔ)對象自身的運(yùn)行時(shí)數(shù)據(jù),如哈希碼(HashCode)、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程 ID、偏向時(shí)間戳等等。
Lock與synchronized的主要區(qū)別:
- 類型:Lock是一個(gè)接口;synchronized是Java中的關(guān)鍵字,synchronized是內(nèi)置的語言實(shí)現(xiàn);
- 異常:Lock在發(fā)生異常時(shí),如果沒有主動(dòng)通過unLock釋放鎖,可能造成死鎖現(xiàn)象,使用Lock時(shí)需要在finally塊中釋放鎖;synchronized在發(fā)生異常時(shí),會(huì)自動(dòng)釋放線程中的占有的鎖,不會(huì)導(dǎo)致死鎖現(xiàn)象發(fā)生。
- 加鎖釋放鎖:synchronized機(jī)制提供了對與每個(gè)對象相關(guān)的隱式的監(jiān)視器鎖,并強(qiáng)制所有鎖的獲取和釋放都必須在同一個(gè)塊結(jié)構(gòu)中。當(dāng)獲取了多個(gè)鎖時(shí),他們必須以相反的順序釋放。即synchronized對于鎖的釋放是隱式的。而Lock機(jī)制必須顯式的調(diào)用Lock對象的unlock()方法,這樣,獲取鎖和釋放鎖可以不在同一個(gè)塊中,這樣可以以更自由的順序釋放鎖。
- 響應(yīng)中斷:Lock可以讓等待鎖的線程響應(yīng)中斷;synchronized不可以讓等待鎖的線程響應(yīng)中斷,等待的線程會(huì)一直等待下去,不能夠響應(yīng)中斷;
- 獲取鎖成功與否:通過Lock可以知道有沒有成功獲取鎖;synchronized不能;
- 讀操作:Lock可以提高多個(gè)線程進(jìn)行讀操作的效率(通過ReadWriteLock);而synchronized避免讀/讀操作
當(dāng)一個(gè)線程進(jìn)入一個(gè)對象的一個(gè)synchronized()方法后,其他線程能進(jìn)入此對象的什么樣的方法?
- 當(dāng)一個(gè)線程在調(diào)用synchronized()方法的過程中,另一個(gè)線程可以訪問同一個(gè)對象的非synchronized()方法;
- 當(dāng)一個(gè)線程在調(diào)用synchronized()方法的過程中,另一個(gè)線程可以訪問靜態(tài)synchronized()方法,因?yàn)殪o態(tài)方法的同步鎖是當(dāng)前類的字節(jié)碼,與非靜態(tài)方法不能同步;
- 當(dāng)一個(gè)線程在調(diào)用synchronized()方法的過程中,在這個(gè)方法內(nèi)部調(diào)用了wait()方法,則另一個(gè)線程就可以訪問同一個(gè)對象的其他synchronized()方法。
什么是同步代碼塊和內(nèi)置鎖?
同步代碼塊
- 同步代碼塊包括兩部分:一個(gè)作為鎖定的對象引用,一個(gè)作為由這個(gè)鎖保護(hù)的代碼塊。
- 靜態(tài)的synchronized方法以Class對象作為鎖;
內(nèi)置鎖
- 每個(gè)Java對象都可以用于做一個(gè)實(shí)現(xiàn)同步的鎖,則這些鎖稱為內(nèi)置鎖或監(jiān)視器鎖。
- 獲得內(nèi)置鎖的唯一途徑是進(jìn)入由這個(gè)鎖保護(hù)的同步代碼塊或方法。
- 內(nèi)置鎖:互斥且可重入。
為什么要?jiǎng)?chuàng)建一種與內(nèi)置鎖相似的新加鎖機(jī)制?
??內(nèi)置鎖能很好的工作,但是在功能上存在一些局限性,如synchronized無法中斷一個(gè)正在等待獲取鎖的線程,或者無法在請求獲取一個(gè)鎖的時(shí)候無限地等待下去,內(nèi)置鎖必須在獲取該鎖的代碼塊中釋放,簡化了編碼工作,且與異常處理操作實(shí)現(xiàn)很好的交互,但無法實(shí)現(xiàn)非阻塞結(jié)構(gòu)的加鎖機(jī)制。所以需要提供一種更靈活的加鎖機(jī)制來提供更好的活躍性或性能。
synchronized和ReentrantLock的抉擇
- 高級(jí)功能:當(dāng)需要可定時(shí)的、可輪詢的、可中斷鎖、公平鎖以及非塊結(jié)構(gòu)的鎖(鎖分段技術(shù))時(shí),才使用ReentrantLock,否則優(yōu)先使用synchronized,畢竟現(xiàn)在JVM內(nèi)置的是synchronized。
- ReentrantLock的危險(xiǎn)性:如果在try-finally中,finally未進(jìn)行unlock,就會(huì)導(dǎo)致鎖沒有釋放,無法追蹤最初發(fā)生錯(cuò)誤的位置。
ReentrantLock中斷和非中斷加鎖區(qū)別?
ReentrantLock的中斷和非中斷加鎖模式的區(qū)別在于:線程嘗試獲取鎖操作失敗后,在等待過程中,如果該線程被其他線程中斷了,它是如何響應(yīng)中斷請求的。lock()方法會(huì)忽略中斷請求,繼續(xù)獲取鎖直到成功;而lockInterruptibly()則直接拋出中斷異常來立即響應(yīng)中斷,由上層調(diào)用者處理中斷。
參考書籍
《Java并發(fā)編程的藝術(shù)》
《Java并發(fā)編程實(shí)戰(zhàn)》
《深入理解Java虛擬機(jī)》