深入理解系列之JAVA多執行緒(2)——synchronized同步原理
多執行緒中為了解決執行緒安全問題,一個重要的手段就是同步!所謂同步其實就是使得原本各個執行緒交叉執行(非同步),變成排隊執行(同步)。同步策略使得不同執行緒操作共享資料遵循“先來後到“,從而避免某個執行緒沒有處理完資料就被另一執行緒搶佔操作出現資料被覆蓋或者髒讀的情況。其中同步最常用的手段就是synchronized關鍵字!
1、synchronized有哪些主要用法?有什麼區別?
synchronized主要有兩種用法,一個是同步方法,另一個是同步程式碼塊!
同步方法在普通方法上加上synchronized關鍵字,預設持有鎖為該例項物件!則如果A執行緒執行該方法,則B執行緒必須等到A執行緒執行完之後才能執行,但是需要注意的有兩點:
···1、A、B兩個執行緒執行該方法所引用的例項物件必須是同一個。如果建立了兩個例項instanceA、instanceB,而A執行緒通過instanceA.method呼叫方法,B執行緒通過instanceB.method呼叫方法,則不能實現同步,因為兩個執行緒持有不同的例項鎖。
···2、同一例項中的方法如果都加了synchronized,那麼預設呼叫該例項下的所有方法都會同步進行(排隊進行),因為該例項下的方法都持有this鎖
···3、對於例項下不加synchronized的其他普通方法,執行緒呼叫不會同步進行!這就會出現安全問題,這是因為同步方法A的程式碼只能阻止另一持有this鎖的同步方法B在A執行的時候不受打擾,卻不能阻止非同步方法在A執行過程中執行!其實道理很簡單,但是敘述看起來複雜一點,所以這裡舉個例子:
public class SynchronizedTest {
public static void main(String[] args) {
Service service = new Service();;
Thread thread1 = new Thread(new Task1(service));
Thread thread2 = new Thread(new Task2(service));
thread1.start();
thread2.start();
}
}
class Service {
private int count = 0;
public synchronized void add() throws InterruptedException {
while (count < 10) {
System.out.println("count=" + count + ",add完之後變成:" + (count + 1));
Thread.sleep(3000);
count++;
System.out.println("count=" + count);
}
}
public int getCount() {
System.out.println("獲取count:" + count);
return count;
}
}
class Task1 implements Runnable {
private Service service;
public Task1(Service service) {
this.service = service;
}
@Override
public void run() {
try {
service.add();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Task2 implements Runnable {
private Service service;
public Task2(Service service) {
this.service = service;
}
@Override
public void run() {
service.getCount();
}
}
例子雖然有點長,但是實際很簡單,有一定基礎的可以很快看明白:一個執行緒執行該例項的同步方法進行自加操作;另一個執行緒執行例項下的非同步方法進行取用操作;因為對自加操作加了同步方法,所以我們本來的目的是利用synchronized的原子性特點等待該函式完全執行完成取用,即結果為100!但是實際結果如下:
count=0,add完之後變成:1
獲取count:0
count=1
count=1,add完之後變成:2
count=2
count=2,add完之後變成:3
也就是說,出現了髒讀!在資料操作過程中就被另一個執行緒取用。解決的方法很簡單就是在getCount方法上加鎖。但是我們從這個例子得出一個什麼容易忽視的結論呢?我們常說synchronized具有原子性,但是實際上這個原子性是相對的:即加鎖的方法原子性是相對於另一持有該鎖的執行緒來說的,卻對不持有該鎖的執行緒無效!
講完同步方法之後,實際基本上synchronized的用法也就清晰了,還需要注意的一點如果同步方法時靜態方法則他的鎖預設是該位元組碼物件。這個不必贅述。接下來就是同步程式碼塊了!因為使用機制其實是一樣的,我們就需要談論一點:為什麼還需要同步程式碼塊?兩個原因:
···1、同步程式碼塊可以指定持有的鎖,不必是預設this這就解決了上述問題中同一例項下的方法如果都是同步方法的話必須同步(順序)執行帶來的效率問題。因為有些方法可能不必等待所有的同步方法執行完畢就可以執行。
···2、同步程式碼塊可以更精細的控制要同步的程式碼範圍!一個方法,為了方法中某一段操作可以同步就得為所有的程式碼塊加鎖,但是有了同步程式碼塊僅僅需要為其中幾行加鎖就行了!要知道同步程式碼的內容越多,就得強制實現順序執行,然後順序執行的效率是很低的!(假設一個方法100行程式碼,只有其中一句i++需要同步,其他操作不需要,那麼如果使用同步方法的方式則必須等待100行程式碼執行完成才能執行其他同步方法,有了同步程式碼塊我僅僅需要執行i++這一行的同步就行了)
所以綜上,同步程式碼塊的出現是為了提高同步的效率問題!
2、synchronized的實現原理是什麼?
synchronized關鍵字使得持有同樣鎖的另一個執行緒必須排隊執行,那麼這個在底層是怎麼實現的呢?談到底層我們不得不再次祭出JVM原理!
synchronized關鍵字在程式碼編譯成位元組碼後會形成兩個位元組碼指令:monitorenter和monitorexit(注:實際通過javap -c測試中並沒有看到這個位元組碼指令,不知道該如何獲得,還求大佬指教)這兩個位元組碼都需要reference型別的引數來指明要鎖定的和解鎖的物件。如果指明那就是這個物件的reference,如果沒有指明則根據是否是靜態方法來決定是例項物件還是類物件!有三點需要注意的:
···1、存在一個鎖計數器,當擁有該鎖則鎖的計數器加1,當釋放該鎖,鎖的計數器減1;
···2、由於synchronized是可重入鎖,即同一執行緒下執行該同步程式碼塊(同步方法)可以在該方法內繼續呼叫其他持有該鎖的同步程式碼塊或者方法,而不會出現互斥同步!所以鎖的計數器可以一直加1,在釋放的時候直到釋放為0才代表真的把鎖完全釋放
···3、同步會使得其他執行緒由執行態變成阻塞狀態,從而等待鎖釋放在執行實現“順序執行”的效果,但是我麼說過java的執行緒實現方式是對映到作業系統上的原生執行緒上的,所以阻塞或者喚醒一個執行緒都需要作業系統來幫忙完成,這就需要從使用者態切換到核心態,但是狀態轉換相對來說會花費很長的時間,比如簡單的setter或者getter操作可能切換的時長要大於方法執行的時長。這也是採用同步導致效率低下的另一重要原因(另一原因就是順序執行帶來的等待消耗)。
注:已經解決為什麼位元組碼指令沒有監視器monitorenter和monitorexit,原因如下:
同步程式碼塊:monitorenter指令插入到同步程式碼塊的開始位置,monitorexit指令插入到同步程式碼塊的結束位置,JVM需要保證每一個monitorenter都有一個monitorexit與之相對應。任何物件都有一個monitor與之相關聯,當且一個monitor被持有之後,他將處於鎖定狀態。執行緒執行到monitorenter指令時,將會嘗試獲取物件所對應的monitor所有權,即嘗試獲取物件的鎖;
同步方法:synchronized方法則會被翻譯成普通的方法呼叫和返回指令如:invokevirtual、areturn指令,在VM位元組碼層面並沒有任何特別的指令來實現被synchronized修飾的方法,而是在Class檔案的方法表中將該方法的access_flags欄位中的synchronized標誌位置1,表示該方法是同步方法並使用呼叫該方法的物件或該方法所屬的Class在JVM的內部物件表示Klass做為鎖物件。
經測試,的確是這樣:
public synchronized void print();
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String XXX
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
public static void main(java.lang.String[]);
Code:
0: ldc #5 // class SynchronizedTest
2: dup
3: astore_1
4: monitorenter
5: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #6 // String YYY
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: new #7 // class SynccompileTest
26: dup
27: invokespecial #8 // Method "<init>":()V
30: invokevirtual #9 // Method print:()V
33: return
Exception table:
from to target type
5 15 18 any
18 21 18 any
}