深刻理解synchronized關鍵字

深刻理解synchronized關鍵字

synchronized是併發編程中重要的使用工具之一,咱們必須學會使用而且掌握它的原理。java

概念及做用

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深度解析》ide

最基本的用法在上一個標題用法中已將僞代碼列出,這裏列舉在以上基礎上稍微變化一些的用法 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.類鎖和無鎖靜態方法,異步執行this

// 將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
使用注意事項 一、鎖對象不能爲空(鎖保存在對象頭中,null沒有對象頭)
二、做用域不宜過大
一、切記要在finally中unlock(),不然會造成死鎖 二、不要將獲取鎖的過程寫在try塊內,由於若是在獲取鎖時發生了異常,異常拋出的同時,也會致使鎖無端被釋放。
相關文章
相關標籤/搜索