我們知道在并發(fā)環(huán)境下為了保證共享變量的線程安全,除了可以使用某些原子類的操作,還可以通過為被保護(hù)的變量加鎖的方式實(shí)現(xiàn)該變量的線程安全。
而在java中我們有兩種方式來使用一個(gè)鎖,請(qǐng)注意,這里所說的鎖都是對(duì)象鎖。一種是JVM幫我們實(shí)現(xiàn)的,通過synchronized關(guān)鍵字來進(jìn)行加鎖,另外一種是J.U.C包中的Lock接口,該接口中的兩個(gè)常用的用來加鎖和解鎖的方法為:lock()、unlock(),除此以外還有其他的用來處理中斷的鎖等等。那么,對(duì)于我們使用者來說,該怎么選擇使用鎖就是一個(gè)需要解決的問題,而要解決這個(gè)問題,首先我們需要對(duì)這兩種鎖的實(shí)現(xiàn)原理進(jìn)行深入的了解。本文筆者將會(huì)深入了解java中的這兩種鎖的實(shí)現(xiàn)原理,以期望得出一種正確使用鎖的方法。
1、synchronized
synchronized是java中的一個(gè)關(guān)鍵字,通過該關(guān)鍵字,我們就可以很方便的對(duì)類方法、實(shí)例方法、代碼塊進(jìn)行加鎖,并且鎖的釋放由JVM來保證,不需要使用者執(zhí)行額外的操作。
Java中的每個(gè)對(duì)象都可以作為鎖。
- 普通同步方法,鎖是當(dāng)前實(shí)例對(duì)象。
- 靜態(tài)同步方法,鎖是當(dāng)前類的class對(duì)象。
- 同步代碼塊,鎖是括號(hào)中的對(duì)象。
我們來看一段代碼:
public class SynchronizedTest {
private static Object object = new Object();
public static void main(String[] args) {
synchronized (object) {
}
}
public synchronized static void m() {}
}
上述代碼中,使用了同步代碼塊和同步方法,我們通過javap -verbose com/github/lock/SynchronizedTest
來查看class文件的信息,以此來分析synchronized關(guān)鍵字的實(shí)現(xiàn)細(xì)節(jié)。
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: getstatic #2 // Field object:Ljava/lang/Object;
3: dup
4: astore_1
5: monitorenter
6: aload_1
7: monitorexit
8: goto 16
11: astore_2
12: aload_1
13: monitorexit
14: aload_2
15: athrow
16: return
public static synchronized void m();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=0, args_size=0
0: return
LineNumberTable:
line 17: 0
從生成的class信息中,可以清楚的看到
- 同步代碼塊使用了 monitorenter 和 monitorexit 指令實(shí)現(xiàn)。
- 同步方法中依靠方法修飾符上的 ACC_SYNCHRONIZED 實(shí)現(xiàn)。
synchronized加鎖解鎖的過程可以描述為下面幾個(gè)步驟:
- 線程執(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)。
但是synchronized并不是對(duì)所有的請(qǐng)求都直接通過使用monitor來進(jìn)行鎖的分配,因?yàn)閙onitor是比較重的鎖,獲取不到鎖的線程將進(jìn)入阻塞狀態(tài),這就涉及到線程狀態(tài)的切換,比較耗資源。synchronized除了實(shí)現(xiàn)了重量級(jí)鎖之外,還實(shí)現(xiàn)了偏向鎖,輕量級(jí)鎖,其中偏向鎖在1.6是默認(rèn)開啟的。
在進(jìn)一步深入之前,我們先認(rèn)識(shí)下兩個(gè)概念:對(duì)象頭和monitor。
1.1、對(duì)象頭
對(duì)象頭包括兩部分:Mark Word (存儲(chǔ)對(duì)象的hashCode或鎖信息等)和 類型指針(存儲(chǔ)到對(duì)象類型數(shù)據(jù)的指針)。如果對(duì)象時(shí)數(shù)組類型,對(duì)象頭還包括數(shù)組的長度,這三個(gè)部分每個(gè)都占32/64bit的長度。
這里我們主要說的是Mark Word。
Mark Word用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù),如哈希碼(HashCode)、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程ID、偏向時(shí)間戳等等,占用內(nèi)存大小與虛擬機(jī)位長一致。
HotSpot通過markOop類型實(shí)現(xiàn)Mark Word,32位虛擬機(jī)的markOop實(shí)現(xiàn)如下:
- hash:保存對(duì)象的哈希碼
- age:保存對(duì)象的分代年齡
- biased_lock:偏向鎖標(biāo)識(shí)位
- lock:鎖狀態(tài)標(biāo)識(shí)位
- JavaThread:保存持有偏向鎖的線程ID
- epoch:保存偏向時(shí)間戳
其中鎖狀態(tài)對(duì)應(yīng)的是biased_lock和lock,不同的鎖狀態(tài),存儲(chǔ)著不同的數(shù)據(jù):
1.2、monitor
monitor是線程私有的數(shù)據(jù)結(jié)構(gòu),每一個(gè)線程都有一個(gè)可用monitor列表,同時(shí)還有一個(gè)全局的可用列表。monitor內(nèi)部組成如下:
- Owner:初始時(shí)為NULL表示當(dāng)前沒有任何線程擁有該monitor,當(dāng)線程成功擁有該鎖后保存線程唯一標(biāo)識(shí),當(dāng)鎖被釋放時(shí)又設(shè)置為NULL。
- EntryQ:關(guān)聯(lián)一個(gè)系統(tǒng)互斥鎖(semaphore),阻塞所有試圖鎖住monitor失敗的線程。
- RcThis:表示blocked或waiting在該monitor上的所有線程的個(gè)數(shù)。
- Nest:用來實(shí)現(xiàn)重入鎖的計(jì)數(shù)。
- HashCode:保存從對(duì)象頭拷貝過來的HashCode值(可能還包含GC age)。
- Candidate:用來避免不必要的阻塞或等待線程喚醒,因?yàn)槊恳淮沃挥幸粋€(gè)線程能夠成功擁有鎖,如果每次前一個(gè)釋放鎖的線程喚醒所有正在阻塞或等待的線程,會(huì)引起不必要的上下文切換(從阻塞到就緒然后因?yàn)楦?jìng)爭(zhēng)鎖失敗又被阻塞)從而導(dǎo)致性能嚴(yán)重下降。Candidate只有兩種可能的值:0表示沒有需要喚醒的線程,1表示要喚醒一個(gè)繼任線程來競(jìng)爭(zhēng)鎖。
那么monitor的作用是什么呢?在 java 虛擬機(jī)中,線程一旦進(jìn)入到被synchronized修飾的方法或代碼塊時(shí),指定的鎖對(duì)象通過某些操作將對(duì)象頭中的Mark Word指向monitor 的起始地址與之關(guān)聯(lián),同時(shí)monitor 中的Owner存放擁有該鎖的線程的唯一標(biāo)識(shí),確保一次只能有一個(gè)線程執(zhí)行該部分的代碼,線程在獲取鎖之前不允許執(zhí)行該部分的代碼。
接下去,我們可以深入了解下在鎖各個(gè)狀態(tài)下,底層是如何處理多線程之間對(duì)鎖的競(jìng)爭(zhēng)。
1.3、鎖的升級(jí)與對(duì)比
Java SE1.6為了減少獲得鎖和釋放鎖帶來的性能消耗,引入了“偏向鎖”和“輕量級(jí)鎖”,在Java SE1.6中,鎖一共有4種狀態(tài),級(jí)別從低到高依次是:無鎖狀態(tài)、偏向鎖狀態(tài)、輕量級(jí)鎖狀態(tài)和重量級(jí)鎖狀態(tài),這幾個(gè)狀態(tài)會(huì)隨著競(jìng)爭(zhēng)情況逐漸升級(jí)。鎖可以升級(jí)但不能降級(jí),意味著偏向鎖升級(jí)成輕量級(jí)鎖后不能降級(jí)成偏向鎖。這種鎖升級(jí)不能降級(jí)的策略,目的是為了提高獲得鎖和釋放鎖的效率。
下面引用一張網(wǎng)上的圖說明白了synchronized的原理。
偏向鎖
HotSpot的作者經(jīng)過研究發(fā)現(xiàn),大多數(shù)情況下,鎖不僅不存在多線程競(jìng)爭(zhēng),而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價(jià)更低而引入了偏向鎖。當(dāng)一個(gè)線程訪問同步塊并獲取鎖時(shí),會(huì)在對(duì)象頭和棧幀中的鎖記錄里存儲(chǔ)鎖偏向的線程ID,以后該線程在進(jìn)入和退出同步塊時(shí)不需要進(jìn)行CAS操作來加鎖和解鎖,只需簡(jiǎn)單地測(cè)試一下對(duì)象頭的Mark Word里是否存儲(chǔ)著指向當(dāng)前線程的偏向鎖。如果測(cè)試成功,表示線程已經(jīng)獲得了鎖。如果測(cè)試失敗,則需要再測(cè)試一下Mark Word中偏向鎖的標(biāo)識(shí)是否設(shè)置成1(表示當(dāng)前是偏向鎖):如果沒有設(shè)置,則使用CAS競(jìng)爭(zhēng)鎖,如果設(shè)置了,則嘗試使用CAS將對(duì)象頭的偏向鎖指向當(dāng)前線程。
偏向鎖在Java6和Java7里是默認(rèn)啟用的,但是它在應(yīng)用程序啟動(dòng)幾秒鐘之后才激活,如果有必要可以使用JVM參數(shù)來關(guān)閉延遲:-XX:BiasedLockingStartupDelay=0。如果你確定應(yīng)用程序里所有的鎖通常情況下處于競(jìng)爭(zhēng)狀態(tài),可以通過JVM參數(shù)關(guān)閉偏向鎖:-XX:-UseBiasedLocking=false,那么程序默認(rèn)會(huì)進(jìn)入輕量級(jí)鎖狀態(tài)。
我們前面說到了synchronized首先會(huì)生成monitorenter指令,我們來看看Hotspot的入口:
UseBiasedLocking標(biāo)識(shí)虛擬機(jī)是否開啟偏向鎖功能,如果開啟則執(zhí)行fast_enter邏輯,否則執(zhí)行slow_enter;
可以看到偏向鎖的處理器邏輯在ObjectSynchronizer::fast_enter
函數(shù)里。
[圖片上傳失敗...(image-32276b-1525442008508)]
偏向鎖的獲取
我們看看revoke_and_rebias方法(因?yàn)樘L,只放出部分重要代碼):
// 獲取對(duì)象的markOop數(shù)據(jù)mark,即對(duì)象頭的Mark Word
markOop mark = obj->mark();
if (mark->has_bias_pattern()) {
Klass* k = obj->klass();
// obj類的對(duì)象頭
markOop prototype_header = k->prototype_header();
if (!prototype_header->has_bias_pattern()) { // 對(duì)象頭里沒有偏向鎖
markOop biased_value = mark;
// CAS替換失敗,說明有多線程競(jìng)爭(zhēng),直接返回BIAS_REVOKED
markOop res_mark = (markOop) Atomic::cmpxchg_ptr(prototype_header, obj->mark_addr(), mark);
assert(!(*(obj->mark_addr()))->has_bias_pattern(), "even if we raced, should still be revoked");
return BIAS_REVOKED;
} else if (prototype_header->bias_epoch() != mark->bias_epoch()) { // 偏向時(shí)間戳改變
// 再偏向
if (attempt_rebias) {
assert(THREAD->is_Java_thread(), "");
markOop biased_value = mark;
markOop rebiased_prototype = markOopDesc::encode((JavaThread*) THREAD, mark->age(), prototype_header->bias_epoch());
// CAS替換成功,返回BIAS_REVOKED_AND_REBIASED,繼續(xù)偏向
markOop res_mark = (markOop) Atomic::cmpxchg_ptr(rebiased_prototype, obj->mark_addr(), mark);
if (res_mark == biased_value) {
return BIAS_REVOKED_AND_REBIASED;
}
}
}
}
實(shí)現(xiàn)邏輯如下:
- 通過
markOop mark = obj->mark()
獲取對(duì)象的markOop數(shù)據(jù)mark,即對(duì)象頭的Mark Word。 - 判斷mark是否為可偏向狀態(tài),即mark的偏向鎖標(biāo)志位為 1,鎖標(biāo)志位為 01。
- 判斷mark中JavaThread的狀態(tài):如果為空,則進(jìn)入步驟(4);如果指向當(dāng)前線程,則執(zhí)行同步代碼塊;如果指向其它線程,進(jìn)入步驟(5)。
- 通過CAS原子指令設(shè)置mark中JavaThread為當(dāng)前線程ID,如果執(zhí)行CAS成功,則執(zhí)行同步代碼塊,否則進(jìn)入步驟(5)。
- 如果執(zhí)行CAS失敗,表示當(dāng)前存在多個(gè)線程競(jìng)爭(zhēng)鎖,當(dāng)達(dá)到全局安全點(diǎn)(safepoint),獲得偏向鎖的線程被掛起,撤銷偏向鎖,并升級(jí)為輕量級(jí),升級(jí)完成后被阻塞在安全點(diǎn)的線程繼續(xù)執(zhí)行同步代碼塊。
偏向鎖的撤銷
只有當(dāng)其它線程嘗試競(jìng)爭(zhēng)偏向鎖時(shí),持有偏向鎖的線程才會(huì)釋放鎖,撤銷的邏輯如下:
- 偏向鎖的撤銷動(dòng)作必須等待全局安全點(diǎn)。
- 暫停擁有偏向鎖的線程,判斷鎖對(duì)象是否處于被鎖定狀態(tài)。
- 撤銷偏向鎖,恢復(fù)到無鎖(標(biāo)志位為 01)或輕量級(jí)鎖(標(biāo)志位為 00)的狀態(tài)。
輕量級(jí)鎖
JVM通過CAS修改對(duì)象頭來獲取鎖,如果CAS修改成功則獲取鎖,如果獲取失敗,則說明有競(jìng)爭(zhēng),則通過CAS自旋一段時(shí)間來修改對(duì)象頭,如果還是獲取失敗,則升級(jí)為重量級(jí)鎖。如果沒有競(jìng)爭(zhēng),輕量級(jí)鎖使用CAS操作避免了使用互斥量的開銷,但如果存在鎖競(jìng)爭(zhēng),除了互斥量的開銷外,還額外發(fā)生了CAS操作,因此在有競(jìng)爭(zhēng)的情況下,輕量級(jí)鎖會(huì)比傳統(tǒng)的重量級(jí)鎖更慢。
重量級(jí)鎖
重量級(jí)鎖通過對(duì)象內(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)的切換,切換成本非常高。
優(yōu)缺點(diǎn)對(duì)比
鎖 | 優(yōu)點(diǎn) | 缺點(diǎn) | 適用場(chǎng)景 |
---|---|---|---|
偏向鎖 | 加鎖和解鎖不需要額外的消耗,和執(zhí)行非同步方法相比僅存在納秒級(jí)的差距 | 如果線程間存在鎖競(jìng)爭(zhēng),會(huì)帶來額外的鎖撤銷的消耗 | 適用于只有一個(gè)線程訪問同步塊的場(chǎng)景 |
輕量級(jí)鎖 | 競(jìng)爭(zhēng)的線程不會(huì)阻塞,提高了程序的響應(yīng)速度 | 如果始終得不到鎖競(jìng)爭(zhēng)的線程,使用自旋會(huì)消耗CPU | 追求響應(yīng)時(shí)間同步塊執(zhí)行速度非常快 |
重量級(jí)鎖 | 線程競(jìng)爭(zhēng)不使用自旋,不會(huì)消耗CPU | 線程阻塞,響應(yīng)時(shí)間緩慢 | 追求吞吐量,同步塊執(zhí)行速度較長 |
1.4、其他概念
鎖消除
鎖消除是指虛擬機(jī)在即時(shí)編譯器在運(yùn)行時(shí),對(duì)于一些在代碼上要求同步,但是被檢測(cè)到不可能存在數(shù)據(jù)競(jìng)爭(zhēng)的鎖進(jìn)行消除。比如,一個(gè)鎖不可能被多個(gè)線程訪問到,那么在這個(gè)鎖上的同步塊JVM將把鎖消除掉。
鎖粗化
程序中一系列的連續(xù)操作都對(duì)同一個(gè)對(duì)象反復(fù)加鎖和解鎖,甚至加鎖操作是出現(xiàn)在循環(huán)體中的,那即使沒有線程競(jìng)爭(zhēng),頻繁地進(jìn)行互斥同步操作也會(huì)導(dǎo)致不必要的性能損耗。
for(int i=0; i<1000; i++){
synchronized(this){
...
}
}
上面代碼JVM將會(huì)優(yōu)化成如下:
synchronized(this){
for(int i=0; i<1000; i++){
...
}
}
2、Lock
Lock是java并發(fā)包J.U.C中的一個(gè)用來實(shí)現(xiàn)鎖的接口,該接口的主要實(shí)現(xiàn)類有ReadLock,WriteLock,ReentrantLock,實(shí)際使用過程中比較常用的就是ReentrantLock。而ReentrantLock主要是基于AQS來完成同步操作的。
Lock有以下幾個(gè)特點(diǎn):
- 可重入:同一個(gè)線程可以多次獲取鎖,不會(huì)阻塞。
- 可中斷:可以使用lock.lockInterruptibly()來響應(yīng)中斷,當(dāng)有中斷出現(xiàn)時(shí),即放棄鎖的請(qǐng)求。
- 公平、非公平:公平鎖講究先來先到,線程在獲取鎖時(shí),如果這個(gè)鎖的等待隊(duì)列中已經(jīng)有線程在等待,那么當(dāng)前線程就會(huì)進(jìn)入等待隊(duì)列中 。非公平鎖不管是否有等待隊(duì)列,如果可以獲取鎖,則立刻占有鎖對(duì)象。
其他的詳細(xì)原理的分析請(qǐng)看我的幾篇文章:【細(xì)談Java并發(fā)】談?wù)凙QS、【細(xì)談Java并發(fā)】談?wù)凴eentrantLock
3、鎖的內(nèi)存語義
內(nèi)存可見性
synchronized關(guān)鍵字強(qiáng)制實(shí)施一個(gè)互斥鎖,使得被保護(hù)的代碼塊在同一時(shí)間只能有一個(gè)線程進(jìn)入并執(zhí)行。當(dāng)然synchronized還有另外一個(gè) 方面的作用:在線程進(jìn)入synchronized塊之前,會(huì)把工作存內(nèi)存中的所有內(nèi)容映射到主內(nèi)存上,然后把工作內(nèi)存清空再從主存儲(chǔ)器上拷貝最新的值。而 在線程退出synchronized塊時(shí),同樣會(huì)把工作內(nèi)存中的值映射到主內(nèi)存,但此時(shí)并不會(huì)清空工作內(nèi)存。這樣一來就可以強(qiáng)制其按照上面的順序運(yùn)行,以 保證線程在執(zhí)行完代碼塊后,工作內(nèi)存中的值和主內(nèi)存中的值是一致的,保證了數(shù)據(jù)的一致性!
所以由synchronized修飾的set與get方法都是相當(dāng)于直接對(duì)主內(nèi)存進(jìn)行操作,不會(huì)出現(xiàn)數(shù)據(jù)一致性方面的問題。
指令重排序
指令重排序是JVM為了優(yōu)化指令,提高程序運(yùn)行效率,在不影響單線程程序執(zhí)行結(jié)果的前提下,盡可能地提高并行度。
synchronized塊對(duì)應(yīng)java程序來說是原子操作,所以說內(nèi)部不管怎么重排序都不會(huì)影響其它線程執(zhí)行導(dǎo)致數(shù)據(jù)錯(cuò)誤,執(zhí)行的指令也不會(huì)溢出方法。
這里放出一張之前分析volatile的圖:
可以看出MonitorEnter(鎖獲取)和volatile讀的語義一致,而MonitorExit(鎖釋放)和volatile寫的語義一致。
下面對(duì)鎖釋放和鎖獲取的內(nèi)存語義做個(gè)總結(jié):
- 線程A釋放一個(gè)鎖,實(shí)質(zhì)上是線程A向接下來將要獲取這個(gè)鎖的某個(gè)線程發(fā)出了(線程A對(duì)共享變量所做修改的)消息。
- 線程B獲取一個(gè)鎖,實(shí)質(zhì)上是線程B接收了之前某個(gè)線程發(fā)出的(在釋放這個(gè)鎖之前對(duì)共享變量所做修改的)消息。
- 線程A釋放鎖,隨后線程B獲取這個(gè)鎖,這個(gè)過程實(shí)質(zhì)上是線程A通過主內(nèi)存向線程B發(fā)送消息。
4、synchronized和lock的區(qū)別
synchronized和lock的區(qū)別如下:
compare | synchronized | lock |
---|---|---|
哪層面 | 虛擬機(jī)層面 | 代碼層面 |
鎖類型 | 可重入、不可中斷、非公平 | 可重入、可中斷、可公平 |
鎖獲取 | A線程獲取鎖,B線程等待 | 可以嘗試獲取鎖,不需要一直等待 |
鎖釋放 | 由JVM 釋放鎖 | 在finally中手動(dòng)釋放。如不釋放,會(huì)造成死鎖 |
鎖狀態(tài) | 無法判斷 | 可以判斷 |
lock比synchronized有如下優(yōu)點(diǎn):
- 支持公平鎖,某些場(chǎng)景下需要獲得鎖的時(shí)間與申請(qǐng)鎖的時(shí)間相一致,但是synchronized做不到 。
- 支持中斷處理,就是說那些持有鎖的線程一直不釋放,正在等待的線程可以放棄等待。如果不支持中斷處理,那么線程可能一直無限制的等待下去,就算那些正在占用資源的線程死鎖了,正在等待的那些資源還是會(huì)繼續(xù)等待,但是ReentrantLock可以選擇放棄等待 。
- condition和lock配合使用,以獲得最大的性能。
5、建議
- 如果沒有特殊的需求,建議使用synchronized,因?yàn)椴僮骱?jiǎn)單,便捷,不需要額外進(jìn)行鎖的釋放。鑒于JDK1.8中的ConcurrentHashMap也使用了CAS+synchronized的方式替換了老版本中使用分段鎖(ReentrantLock)的方式,可以得知,JVM中對(duì)synchronized的性能做了比較好的優(yōu)化。
- 如果代碼中有特殊的需求,建議使用Lock。例如并發(fā)量比較高,且有些操作比較耗時(shí),則可以使用支持中斷的所獲取方式;如果對(duì)于鎖的獲取,講究先來后到的順序則可以使用公平鎖;另外對(duì)于多個(gè)變量的鎖保護(hù)可以通過lock中提供的condition對(duì)象來和lock配合使用,獲取最大的性能。