介紹
JDK提供的鎖分兩種,一種是JVM實現(xiàn)的synchronized,是java的關(guān)鍵字,因此在這個關(guān)鍵字作用對象的范圍內(nèi)都是可以保證原子性的,主要是依賴特殊的CPU指令。另一種是JDK提供的代碼層面的鎖Lock。
一、synchronized的四種用法
1. 修飾代碼塊
大括號括起來的代碼,稱同步語句塊,作用范圍是大括號,作用對象是調(diào)用代碼塊的對象。
public void test1(int j) {
synchronized (this) {
for (int i = 0; i < 10; i++) {
log.info("test1 {} - {}", j, i);
}
}
}
測試代碼:
public static void main(String[] args) {
SynchronizedExample1 example1 = new SynchronizedExample1();
SynchronizedExample1 example2 = new SynchronizedExample1();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() - > {
example1.test1(1);
});
executorService.execute(() - > {
example2.test1(2);
});
}
測試結(jié)果:
- 可以看到test1方法的參數(shù)1和參數(shù)2是交替執(zhí)行。
2. 修飾方法
被修飾的方法稱為同步方法,作用范圍是整個方法,作用于調(diào)用對象。
public synchronized void test2(int j) {
for (int i = 0; i < 10; i++) {
log.info("test2 {} - {}", j, i);
}
}
測試代碼:
public static void main(String[] args) {
SynchronizedExample1 example1 = new SynchronizedExample1();
SynchronizedExample1 example2 = new SynchronizedExample1();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() - > {
example1.test2(1);
});
executorService.execute(() - > {
example2.test2(2);
});
}
測試結(jié)果:
- 可以看到test2方法的參數(shù)1和參數(shù)2是交替執(zhí)行。
3. 修飾靜態(tài)方法
作用范圍是整個方法,作用于所有對象。
public static synchronized void test3(int j) {
for (int i = 0; i < 10; i++) {
log.info("test3 {} - {}", j, i);
}
}
測試代碼:
public static void main(String[] args) {
SynchronizedExample1 example1 = new SynchronizedExample1();
SynchronizedExample1 example2 = new SynchronizedExample1();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() - > {
example1.test3(1);
});
executorService.execute(() - > {
example2.test3(2);
});
}
測試結(jié)果:
- 可以看到test3方法的參數(shù)1和參數(shù)2是1執(zhí)行完才執(zhí)行的2。
4. 修飾類
作用范圍是synchronized后面括號括起來的部分,作用于所有對象。
public static void test4(int j) {
synchronized (SynchronizedExample2.class) {
for (int i = 0; i < 10; i++) {
log.info("test4 {} - {}", j, i);
}
}
}
測試代碼:
public static void main(String[] args) {
SynchronizedExample1 example1 = new SynchronizedExample1();
SynchronizedExample1 example2 = new SynchronizedExample1();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() - > {
example1.test4(1);
});
executorService.execute(() - > {
example2.test4(2);
});
}
測試結(jié)果:
- 可以看到test4方法的參數(shù)1和參數(shù)2是1執(zhí)行完才執(zhí)行的2。
二、synchronized的原理
在Java語言中存在兩種內(nèi)建的synchronized語法:synchronized語句、synchronized方法:
- synchronized語句:當(dāng)源代碼被編譯成字節(jié)碼的時候,會在同步塊的入口位置和退出位置分別插入monitorenter和monitorexit字節(jié)碼指令。
- synchronized方法:在Class文件的方法表中將該方法的access_flags字段中的synchronized標(biāo)志位置1
synchronized語句
如上示例,test1和test4使用的就是synchronized語句。使用Javap -c命令反編譯test1代碼,如下:
在Java虛擬機(jī)的specification中,有關(guān)于monitorenter和monitorexit字節(jié)碼指令的詳細(xì)描述:
monitorenter
每個對象都有一個鎖,也就是監(jiān)視器(monitor)。 Monitor可以理解為一個同步工具或一種同步機(jī)制,通常被描述為一個對象。 每一個Java對象就有一把看不見的鎖,稱為內(nèi)部鎖或者M(jìn)onitor鎖。
當(dāng)monitor被占有時就表示它被鎖定。線程執(zhí)行monitorenter指令時嘗試獲取對象所對應(yīng)的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)。
monitorexit
執(zhí)行monitorexit的線程必須是相應(yīng)的monitor的所有者。指令執(zhí)行時,monitor的進(jìn)入數(shù)減1,如果減1后進(jìn)入數(shù)為0,那線程退出monitor,不再是這個monitor的所有者。其他被這個monitor阻塞的線程可以嘗試去獲取這個monitor的所有權(quán)。
synchronized方法
如上示例,test2和test3使用的就是synchronized方法。synchronized方法加鎖的方式是在Class文件的方法表中將該方法的access_flags字段中的synchronized標(biāo)志位置1。如下:
- 訪問標(biāo)志的第11位即為加鎖標(biāo)記位。
三、synchronized的優(yōu)化
synchronized在操作同步資源之前需要給同步資源先加鎖,這把鎖就是存在Java對象頭里,而Java對象頭又是什么呢?
Java對象頭
以Hotspot虛擬機(jī)為例,Hotspot的對象頭主要包括兩部分?jǐn)?shù)據(jù):Mark Word(標(biāo)記字段)、Klass Pointer(類型指針)。
Mark Word
默認(rèn)存儲對象的HashCode,分代年齡和鎖標(biāo)志位信息。這些信息都是與對象自身定義無關(guān)的數(shù)據(jù),所以Mark Word被設(shè)計成一個非固定的數(shù)據(jù)結(jié)構(gòu)以便在極小的空間內(nèi)存存儲盡量多的數(shù)據(jù)。它會根據(jù)對象的狀態(tài)復(fù)用自己的存儲空間,也就是說在運行期間Mark Word里存儲的數(shù)據(jù)會隨著鎖標(biāo)志位的變化而變化。
Klass Point
對象指向它的類元數(shù)據(jù)的指針,虛擬機(jī)通過這個指針來確定這個對象是哪個類的實例。
在JDK1.6及其之前的版本中monitorenter和monitorexit字節(jié)碼依賴于底層的操作系統(tǒng)的Mutex Lock來實現(xiàn)的,但是由于使用Mutex Lock需要將當(dāng)前線程掛起并從用戶態(tài)切換到內(nèi)核態(tài)來執(zhí)行,這種切換的代價是非常昂貴的。然而在現(xiàn)實中的大部分情況下,同步方法是運行在單線程環(huán)境(無鎖競爭環(huán)境)。如果每次都調(diào)用Mutex Lock將嚴(yán)重的影響程序的性能。因此在JDK6中為了減少獲得鎖和釋放鎖帶來的性能消耗,引入了偏向鎖和 輕量級鎖 。所以目前鎖一共有4種狀態(tài),級別從低到高依次是:無鎖、偏向鎖、輕量級鎖和重量級鎖。鎖狀態(tài)只能升級不能降級。如下:
無鎖
- 無鎖沒有對資源進(jìn)行鎖定,所有的線程都能訪問并修改同一個資源,但同時只有一個線程能修改成功。
- 無鎖的特點就是修改操作在循環(huán)內(nèi)進(jìn)行,線程會不斷的嘗試修改共享資源。如果沒有沖突就修改成功并退出,否則就會繼續(xù)循環(huán)嘗試。如果有多個線程修改同一個值,必定會有一個線程能修改成功,而其他修改失敗的線程會不斷重試直到修改成功。
偏向鎖
- 偏向鎖是指一段同步代碼一直被一個線程所訪問,那么該線程會自動獲取鎖,降低獲取鎖的代價。
- 引入偏向鎖是為了在無多線程競爭的情況下盡量減少不必要的輕量級鎖執(zhí)行路徑,其目標(biāo)就是在只有一個線程執(zhí)行同步代碼塊時能夠提高性能。
- 當(dāng)一個線程訪問同步代碼塊并獲取鎖時,會在Mark Word里存儲鎖偏向的線程ID。在線程進(jìn)入和退出同步塊時不再通過CAS操作來加鎖和解鎖,而是檢測Mark Word里是否存儲著指向當(dāng)前線程的偏向鎖。
- 偏向鎖只有遇到其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程才會釋放鎖,線程不會主動釋放偏向鎖。偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有字節(jié)碼正在執(zhí)行),它會首先暫停擁有偏向鎖的線程,判斷鎖對象是否處于被鎖定狀態(tài)。撤銷偏向鎖后恢復(fù)到無鎖或輕量級鎖的狀態(tài)。
- 偏向鎖在JDK 6及以后的JVM里是默認(rèn)啟用的。可以通過JVM參數(shù)關(guān)閉偏向鎖: -XX:-UseBiasedLocking=false ,關(guān)閉之后程序默認(rèn)會進(jìn)入輕量級鎖狀態(tài)。
輕量級鎖
- 是指當(dāng)鎖是偏向鎖的時候,被另外的線程所訪問,偏向鎖就會升級為輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞,從而提高性能。
- 在代碼進(jìn)入同步塊的時候,如果同步對象鎖狀態(tài)為無鎖狀態(tài)(鎖標(biāo)志位為01狀態(tài),是否為偏向鎖為0),虛擬機(jī)首先將在當(dāng)前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用于存儲鎖對象目前的Mark Word的拷貝,然后拷貝對象頭中的Mark Word復(fù)制到鎖記錄中。
- 拷貝成功后,虛擬機(jī)將使用CAS操作嘗試將對象的Mark Word更新為指向Lock Record的指針,并將Lock Record里的owner指針指向?qū)ο蟮腗ark Word。
- 如果這個更新動作成功了,那么這個線程就擁有了該對象的鎖,并且對象Mark Word的鎖標(biāo)志位設(shè)置為00,表示此對象處于輕量級鎖定狀態(tài)。
- 如果輕量級鎖的更新操作失敗了,虛擬機(jī)首先會檢查對象的Mark Word是否指向當(dāng)前線程的棧幀,如果是就說明當(dāng)前線程已經(jīng)擁有了這個對象的鎖,那就可以直接進(jìn)入同步塊繼續(xù)執(zhí)行,否則說明多個線程競爭鎖。
重量級鎖
- 若當(dāng)前只有一個等待線程,則該線程通過自旋進(jìn)行等待。但是當(dāng)自旋超過一定的次數(shù),或者一個線程在持有鎖,一個在自旋,又有第三個來訪時,輕量級鎖升級為重量級鎖。
- 升級為重量級鎖時,鎖標(biāo)志的狀態(tài)值變?yōu)?0,此時Mark Word中存儲的是指向重量級鎖的指針,此時等待鎖的線程都會進(jìn)入阻塞狀態(tài)。
綜上,偏向鎖通過對比Mark Word解決加鎖問題,避免執(zhí)行CAS操作。而輕量級鎖是通過用CAS操作和自旋來解決加鎖問題,避免線程阻塞和喚醒而影響性能。重量級鎖是將除了擁有鎖的線程以外的線程都阻塞。
四、synchronized存在的問題
1.性能損耗
- 雖然在JDK 1.6中對synchronized做了很多優(yōu)化,如如適應(yīng)性自旋、鎖消除、鎖粗化、輕量級鎖和偏向鎖等,但畢竟還是一種鎖。
- 所以,無論是使用同步方法還是同步代碼塊,在同步操作之前還是要進(jìn)行加鎖,同步操作之后需要進(jìn)行解鎖,這個加鎖、解鎖的過程是要有性能損耗的。
2. 阻塞
- synchronize實現(xiàn)的鎖本質(zhì)上是一種阻塞鎖,多個線程要排隊訪問同一個共享對象。
-
JAVA語言
+關(guān)注
關(guān)注
0文章
138瀏覽量
20076 -
JVM
+關(guān)注
關(guān)注
0文章
157瀏覽量
12209 -
虛擬機(jī)
+關(guān)注
關(guān)注
1文章
908瀏覽量
28095 -
CAS
+關(guān)注
關(guān)注
0文章
34瀏覽量
15183
發(fā)布評論請先 登錄
相關(guān)推薦
評論