深入理解synchronized關鍵字

Marksman發表於2019-01-08

深入理解synchronized關鍵字

synchronized是併發程式設計中重要的使用工具之一,我們必須學會使用並且掌握它的原理。

概念及作用

JVM自帶的關鍵字,可在需要執行緒安全的業務場景中使用,來保證執行緒安全。

用法

按照鎖的物件區分可以分為物件鎖類鎖 按照在程式碼中的位置區分可以分為方法形式程式碼塊形式

物件鎖

鎖物件為當前this或者說是當前類的例項物件

public void synchronized method() {
    System.out.println("我是普通方法形式的物件鎖");
}

public void method() {
    synchronized(this) {
        System.out.println("我是程式碼塊形式的物件鎖");
    }
}
複製程式碼

類鎖

鎖的是當前類或者指定類的Class物件。一個類可能有多個例項物件,但它只可能有一個Class物件。

public static void synchronized method() {
    System.out.println("我是靜態方法形式的類鎖");
}

public void method() {
    synchronized(*.class) {
        System.out.println("我是程式碼塊形式的類鎖");
    }
}
複製程式碼

SimpleExample

參考 慕課網 《Java高併發之魂:synchronized深度解析》

最基本的用法在上一個標題用法中已將虛擬碼列出,這裡列舉在以上基礎上稍微變化一些的用法 1.多個例項,對當前例項加鎖,同步執行,對當前類Class物件加鎖,非同步執行

public class SimpleExample implements Runnable {
    static SimpleExample instance1 = new SimpleExample();
    static SimpleExample instance2 = new SimpleExample();
    
    @Override
    public void run() {
        method1();
        method2();
        method3();
        method4();
    }
    
    public synchronized void method1() {
        common();
    }
    
    public static synchronized void method2() {
       commonStatic();
    }
    
    public void method3() {
        synchronized(this) {
            common();
        }
    }

    public void method4() {
        synchronized(MultiInstance.class) {
            common();
        }
    }
    
    public void method5() {
        common();
    }
    
    public void method6() {
        commonStatic();
    }
    
    public void common() {
        System.out.println("執行緒 " + Thread.currentThread().getName() + " 正在執行");
        try {
            Thread.sleep(1000);
        } catch(InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("執行緒 " + Thread.currentThread().getName() + " 執行完畢");
    }
    
    public static void commonStatic() {
        System.out.println("執行緒 " + Thread.currentThread().getName() + " 正在執行");
        try {
            Thread.sleep(1000);
        } catch(InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("執行緒 " + Thread.currentThread().getName() + " 執行完畢");
    }
    
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(instance1);
        Thread t2 = new Thread(instance2);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("finished");
    }
}

method1()、method3()結果為:
    執行緒 Thread-0 正在執行
    執行緒 Thread-1 正在執行
    執行緒 Thread-0 執行完畢
    執行緒 Thread-1 執行完畢
    finished
    
method2()method4()執行結果為:
    執行緒 Thread-0 正在執行
    執行緒 Thread-0 執行完畢
    執行緒 Thread-1 正在執行
    執行緒 Thread-1 執行完畢
    finished
複製程式碼

2.物件鎖和類鎖,鎖的物件不一樣,互不影響,所以非同步執行

// 將run方法改為
@Override
public void run() {
    if("Thread-0".equals(Thread.currentThread().getName())) {
        method1();   
    } else {
        method2();
    }
}
// 將main方法改為
public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(instance1);
    Thread t2 = new Thread(instance1);
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println("finished");
}
結果為:
    執行緒 Thread-0 正在執行
    執行緒 Thread-1 正在執行
    執行緒 Thread-1 執行完畢
    執行緒 Thread-0 執行完畢
    finished
複製程式碼

3.物件鎖和無鎖得普通方法,普通方法不需要持有鎖,所以非同步執行

// 將run方法改為
@Override
public void run() {
    if("Thread-0".equals(Thread.currentThread().getName())) {
        method1();   
    } else {
        method5();
    }
}
// main方法同 2
結果為:
    執行緒 Thread-0 正在執行
    執行緒 Thread-1 正在執行
    執行緒 Thread-0 執行完畢
    執行緒 Thread-1 執行完畢
    finished
複製程式碼

4.類鎖和無鎖靜態方法,非同步執行

// 將run方法改為
@Override
public void run() {
    if("Thread-0".equals(Thread.currentThread().getName())) {
        method1();   
    } else {
        method6();
    }
}
// main方法同 2
結果為:
    執行緒 Thread-0 正在執行
    執行緒 Thread-1 正在執行
    執行緒 Thread-0 執行完畢
    執行緒 Thread-1 執行完畢
    finished
複製程式碼

5.方法丟擲異常,synchronized鎖自動釋放

// run方法改為
@Override
public void run() {
    if ("Thread-0".equals(Thread.currentThread().getName())) {
        method7();
    } else {
        method8();
    }
}

public synchronized void method7() {
    ...
    throw new RuntimeException();
}

public synchronized void method8() {
    common();
}

public static void main(String[] args) throws InterruptedException {
    // 同 2
}

結果為:
    我是丟擲異常的加鎖方法,我叫 thread---------1
    我是沒有異常的加鎖方法,我叫 thread---------2
    Exception in thread "thread---------1" java.lang.RuntimeException
	    at com.marksman.theory2practicehighconcurrency.synchronizedtest.SynchronizedException9.method1(SynchronizedException9.java:29)
	    at com.marksman.theory2practicehighconcurrency.synchronizedtest.SynchronizedException9.run(SynchronizedException9.java:15)
	    at java.lang.Thread.run(Thread.java:748)
    thread---------2 執行結束
    finished
// 這說明丟擲異常後持有物件鎖的method7()方法釋放了鎖,這樣method8()才能獲取到鎖並執行。
複製程式碼

6.可重入特性

public class SynchronizedRecursion {
    int a = 0;
    int b = 0;
    private void method1() {
        System.out.println("method1正在執行,a = " + a);
        if (a == 0) {
            a ++;
            method1();
        }
        System.out.println("method1執行結束,a = " + a);
    }
    
    private synchronized void method2() {
        System.out.println("method2正在執行,b = " + b);
        if (b == 0) {
            b ++;
            method2();
        }
        System.out.println("method2執行結束,b = " + b);
    }

    public static void main(String[] args) {
        SynchronizedRecursion synchronizedRecursion = new SynchronizedRecursion();
        synchronizedRecursion.method1();
        synchronizedRecursion.method2();
    }
}
結果為:
    method1正在執行,a = 0
    method1正在執行,a = 1
    method1執行結束,a = 1
    method1執行結束,a = 1
    
    method2正在執行,b = 0
    method2正在執行,b = 1
    method2執行結束,b = 1
    method2執行結束,b = 1
    
// 可以看到method1()與method2()的執行結果一樣的,method2()在獲取到物件鎖以後,在遞迴呼叫時不需要等上一次呼叫先釋放後再獲取,而是直接進入,這說明了synchronized的可重入性.
// 當然,除了遞迴呼叫,呼叫同類的其它同步方法,呼叫父類同步方法,都是可重入的,前提是同一物件去呼叫,這裡就不一一列舉了.
複製程式碼

總結一下

  • 一把鎖只能同時被一個執行緒獲取,沒有拿到鎖的執行緒必須等待;
  • 每個例項都對應有自己的一把鎖,不同例項之間互不影響;
  • 鎖物件是*.class以及synchronized修飾的static方法時,所有物件共用一把類鎖;
  • 無論是方法正常執行完畢或者方法丟擲異常,都會釋放鎖;
  • 使用synchronized修飾的方法都是可重入的。

synchronized的實現原理

monitorenter和monitorexit

將下面兩段程式碼分別用 javac *.java編譯成.class檔案,再反編譯 javap -verbose *.class檔案

public class SynchronizedThis {
	public void method() {
		synchronized(this) {}
	}
}

// 反編譯結果
public void method();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter
         4: aload_1
         5: monitorexit
         6: goto          14
         9: astore_2
        10: aload_1
        11: monitorexit
        12: aload_2
        13: athrow
        14: return
複製程式碼
public class SynchronizedMethod {
	public synchronized void method() {}
}

// 反編譯結果
public synchronized void method();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 2: 0
複製程式碼

可以看到:

  • synchronized加在程式碼塊上,JVM是通過monitorentermonitorexit來控制鎖的獲取的釋放的;
  • synchronized加在方法上,JVM是通過ACC_SYNCHRONIZED標記來控制的,但本質上也是通過monitorenter和monitorexit指令控制的。

物件頭

參考 《Java物件頭詳解》

上面我們提到monitor,這是什麼鬼? 其實,物件在記憶體是這樣儲存的,包括物件頭例項資料對齊填充Padding,其中物件頭包括 Mark Word和型別指標。

Mark Word

Mark Word用於儲存物件自身的執行時資料,如雜湊碼(identity_hashcode)、GC分代年齡(age)、鎖狀態標誌(lock)、執行緒持有的鎖、偏向執行緒ID(thread)、偏向時間戳(epoch)等等,佔用記憶體大小與虛擬機器位長一致。

Mark Word (32 bits) State 鎖狀態
identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 Normal 無鎖
thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 Biased 偏向鎖
ptr_to_lock_record:30 | lock:2 Lightweight Locked 輕量級鎖
ptr_to_heavyweight_monitor:30 | lock:2 Heavyweight Locked 重量級鎖
| lock:2 Marked for GC GC標記
Mark Word (64 bits) State 鎖狀態
unused:25|identity_hashcode:31|unused:1|age:4|biased_lock:1|lock:2 Normal 無鎖
thread:54 |epoch:2|unused:1|age:4|biased_lock:1|lock:2 Biased 偏向鎖
ptr_to_lock_record:62 | lock:2 Lightweight Locked 輕量級鎖
ptr_to_heavyweight_monitor:62 | lock:2 Heavyweight Locked 重量級鎖
| lock:2 Marked for GC GC標記

可以看到,monitor就存在Mark Word中。

型別指標

型別指標指向物件的類後設資料metadata,虛擬機器通過這個指標確定該物件是哪個類的例項。

鎖狀態

biased_lock lock 狀態
0 01 無鎖
1 01 偏向鎖
0 00 輕量級鎖
0 10 重量級鎖
0 11 GC標記

JDK對synchronized的優化

jdk1.6之前synchronized是很重的,所以並不被開發者偏愛,隨著後續版本jdk對synchronized的優化使其越來越輕量,它還是很好用的,甚至ConcurrentHashMap在jdk的put方法都在jdk1.8時從ReetrantLock.tryLock()改為用synchronized來實現同步。 並且還引入了偏向鎖,輕量級鎖等概念,下面是偏向鎖和輕量級鎖的獲取流程

www.processon.com/diagraming/…

參考 《咕泡學院公開課》 提取碼:s6vx

偏向鎖 baised_lock

如果一個執行緒獲取了偏向鎖,那麼如果在接下來的一段時間裡,如果沒有其他執行緒來搶佔鎖,那麼獲取鎖的執行緒在下一次進入方法時不需要重新獲取鎖。

synchronized與ReentrantLock的區別

參考 極客時間《Java核心技術36講專欄》

區別 synchronized ReentrantLock
靈活性 程式碼簡單,自動獲取、釋放鎖 相對繁瑣,需要手動獲取、釋放鎖
是否可重入
作用位置 可作用在方法和程式碼塊 只能用在程式碼塊
獲取、釋放鎖的方式 monitorenter、monitorexit、ACC_SYNCHRONIZED 嘗試非阻塞獲取鎖tryLock()、超時獲取鎖tryLock(long timeout,TimeUnit unit)、unlock()
獲取鎖的結果 不知道 可知,tryLock()返回boolean
使用注意事項 1、鎖物件不能為空(鎖儲存在物件頭中,null沒有物件頭)
2、作用域不宜過大
1、切記要在finally中unlock(),否則會形成死鎖
2、不要將獲取鎖的過程寫在try塊內,因為如果在獲取鎖時發生了異常,異常丟擲的同時,也會導致鎖無故被釋放。

相關文章