你必須知道的Synchronized (前篇:底層實現)

關鍵字Synchronized的做用是實現線程間的同步,下面就簡稱sync。java

sync要聊的東西太多了,本節先聊sync的底層實現和簡單區別,後面還會寫出Synchronized的鎖升級過程和相關優化。bash

1.sync的使用

`測試

private static CountDownLatch latch = new CountDownLatch(100);    
public static void main(String[] args) throws Exception {        
    T t = new T();        
    for (int i = 0; i < 100; i++) {           
        new Thread(() -> {                
            for (int j = 0; j < 100; j++) {                    
             // 輸出值 老是等於 10000
             // t.safeIncr();
             // t.safeIncrBlock();
             // 輸出值老是小於 10000 
                t.unSafeIncr();                
            }                
            latch.countDown();            
        }).start();        
    }        
    // 爲了等待100個線程全都執行完成 
    latch.await();        
    System.out.println(t.count);    
}    
private static class T {       
    private int count;        
    public void unSafeIncr(){            
        count ++;        
    }  
    public void safeIncrBlock(){
        synchronized(this){
            count ++;
        }
    }
    public synchronized void safeIncr(){            
        count ++;        
    }    
}
複製代碼

`優化

上面的例子展現了sync的兩種使用方式,即:ui

一、在方法簽名加上sync關鍵字this

二、使用sync代碼塊atom

兩種方式都能達到 最後想要的結果,count=10000spa

2.sync的底層實現

這裏分兩個方面來說:線程

1.從底層實現上來講:code

同步代碼塊:採用monitorentermonitorexit兩個指令來實現同步

同步方法: 採用ACC_SYNCHRONIZED標記符來實現同步

1.同步代碼塊

把上面代碼 使用 javap -c -v '.\Sync_Method_01$T.class' 來反編譯看一下字節碼:

public void safeIncrBlock();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=3, args_size=1
         0: aload_0				// 局部變量表第0位this壓棧
         1: dup					// 複製this引用,併入棧
         2: astore_1			// 把this彈出棧,存入第一個局部變量
         3: monitorenter		// 根據this引用進行加鎖
         4: aload_0			
         5: dup
         6: getfield      #1                  // Field count:I
         9: iconst_1
        10: iadd
        11: putfield      #1                  // Field count:I
        14: aload_1
        15: monitorexit			// 釋放鎖,在前面4-14行已經完成了 count ++ 的操做
        16: goto          24	// 跳轉到 24行,退出
        19: astore_2			// 若是在 4-16 行遇到任何的Exception,則會進入到19行處理
        20: aload_1				// 第一個局部變量入棧
        21: monitorexit			// 根據 棧頂的 this 退出臨界區,釋放鎖	
        22: aload_2				// 第二個局部變量入棧
        23: athrow				// 拋出異常
        24: return
複製代碼

經過字節碼,能夠看到兩個指令:monitorenter 和 monitorexit, 在JVM規範中也有相應的解釋,有興趣的能夠看一下,大概意思就是,JVM會經過sync鎖住的對象去找到對應的monitor(能夠理解爲一個監視計數器),而後根據monitor的狀態進行加解鎖的判斷,若是一個線程嘗試獲取鎖,而且對象的monitor爲0,則當前線程被准許執行後續代碼,而後將monitor作一次自增,此時爲1,若是當前線程又一次獲取對象鎖(sync是可重入鎖),monitor繼續作自增,此時爲2,當線程執行完成任務以後,須要使用monitorexit聲明退出釋放鎖,則monitor自減,若是在這中間有其餘線程也來嘗試獲取鎖,可是monitor不爲0,則其餘線程須要進行等待;在JVM中,任何一個對象自身都會有一個監視器與其關聯,用來判斷當前對象是否被鎖定,當監視器被持有後,當前對象就會處於鎖定的狀態。

2.同步方法:

一樣仍是看反編譯以後的字節碼:

public synchronized void safeIncr();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED		// 同步方法修飾符
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #1 // Field count:I
         5: iconst_1
         6: iadd
         7: putfield      #1 // Field count:I
        10: return
      LineNumberTable:
        line 42: 0
        line 43: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Latomicity/juejin/Sync_Method_01$T;
複製代碼

JVM對同步方法的處理是經過使用 ACC_SYNCHRONIZED來支持的,能夠看到同步方法通過反編譯以後的字節碼跟普通的無同步代碼沒什麼區別,並無顯示的使用monitorenter 和 monitorexit來進行同步,對於同步方法來講,當JVM經過方法的標識符來判斷它是否是同步方法的時候,會自動的在調用的方法前面進行加鎖操做,當同步方法執行完以後,不論是正常的退出仍是拋異常,都會由JVM來釋放鎖,也就是說,ACC_SYNCHRONIZED意味着JVM會隱式的(使用方法調用和返回指令)使用monitorenter和monitorexit

JVM規範原文以下:

3.sync的做用區別

1.sync指定加鎖對象,線程進入同步代碼塊前,必需要獲取給定的對象的鎖

2.做用在普通方法上,至關於對當前對象實例進行加鎖,線程若是要執行同步方法,必須得到當前對象實例的鎖

3.做用在靜態方法上,至關於對當前類加鎖,線程若是要執行同步方法,必須獲取當前類的鎖

1,2很簡單你們都懂,這裏針對第三點 靜態方法 舉例說明:

public class Sync_Method_02 {
    public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(() -> {
            T2.t1();
        });
        t1.start();
      
        // 這裏測試 靜態同步方法 和 普通同步方法 之間是否有競爭狀況
        Thread t3 = new Thread(() -> new T2().t3());
        t3.start();
        
        Thread t4 = new Thread(() -> T2.t4());
        t4.start();
        
        // 爲了確保 t1線程會先執行
        TimeUnit.SECONDS.sleep(1);
        for (int i = 0; i < 10; i++) {
            new Thread(() -> T2.t2()).start();
        }
    }
    private static class T2 {
        
        public static synchronized void t1() {
            try {
                System.out.println("t1 start");
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t1 end...");
        }
        
        public static synchronized void t2() {
            System.out.println("t2 start");
        }
        
        public synchronized void t3(){
            try {
                System.out.println("t3 start");
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t3 end");
        }
        
        public static void t4() {
            // 至關於 靜態同步方法
            synchronized (T2.class){
                System.out.println("t4 ...");
            }
        }
    }
}
複製代碼

運行上面的代碼,能夠發現 t1或者t3方法老是先進行打印的,也就是說當t1線程在執行 T2.t1() 同步方法時,t1會首先獲取T2的鎖,一秒事後其餘10個線程開始執行T2.t2(), 可是此時鎖被t1線程持有,因此其餘10個線程必須等t1線程釋放T2的鎖,纔有機會去執行。說明全部的靜態同步方法用的也是同一把鎖——類對象自己。

而靜態同步方法和非靜態同步方法這兩把鎖是兩個不一樣的對象,因此靜態同步方法與非靜態同步方法之間是不會有競爭的。

PS:原理就先寫這麼多吧,不少細節後面還會聊,下節會聊鎖升級的詳細過程和JVM相關優化節,小白第一次寫技術博客,沒經驗,有問題的話,但願各位大神指出,有錯必改!

相關文章
相關標籤/搜索