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