【Java多線程與併發】——Synchronized關鍵字詳解

目錄

一、syncronized關鍵字介紹

二、synchronized使用場景

三 、synchronized的一些特性

1)synchronized具有鎖重入的功能

2)同步不具有繼承性

3)退出或者異常發生時自動釋放鎖

4)synchronized(string)使用的注意

5)synchronized使用基本原則

四、synchronized實現原理及應用

1)同步方法實現原理

2)同步代碼塊實現原理


一、syncronized關鍵字介紹

synchronized關鍵字是用來解決Java語言中非線程安全的相關問題,一句話說就是synchronized是爲了解決多個線程訪問資源的同步性。

二、synchronized使用場景

synchronized既可以用來修飾方法,也可以用來修飾代碼塊,如下圖(此圖來源於:讓你徹底理解Synchronized)所示:

如上圖所示 :當synchronized修飾實例方法時,鎖的是類的實例對象,而修飾靜態方法時,鎖的是整個類對象,即對類的所有實例對象都起同步的效果。

需要知道的是:

  • 多個線程調用同一個對象中的不同名稱的synchronized同步方法或synchronized(this)同步代碼塊時,調用的效果就是同步的
  • 對於同一個對象,當一個線程對象的同步方法時,其他線程可以調用對象的非同步方法,而不會發生同步阻塞

三 、synchronized的一些特性

1)synchronized具有鎖重入的功能

比如說當一個線程獲得了某個對象鎖,此時這個對象鎖還沒有釋放,當其想再此獲得鎖時還是可以獲取的,如下代碼示例:

class Service {
    synchronized public void serviceA(){
        System.out.println("serviceA");
        serviceB();
    }
    synchronized public void serviceB(){
        System.out.println("serviceB");
        serviceC();
    }
    synchronized public void serviceC(){
        System.out.println("serviceC");
    }
}
public class MyThread extends Thread{
    @Override
    public void run() {
        Service service = new Service();
        service.serviceA();
    }
}
public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
}

運行結果:

 

當存在父子類繼承關係時,子類完全可以通過“可重入鎖”調用父類的同步方法

2)同步不具有繼承性

當子類重寫了父類的同步方法,但是沒有加synchronized關鍵字,即使對於同一個對象,多線程調用子類方法,子類方法調用父類方法時,此時子類方法的執行和父類的同步方法執行時是異步的效果,而非同步。

3)退出或者異常發生時自動釋放鎖

當一個線程執行一個方法出現異常時,其所持有的鎖會自動釋放

4)synchronized(string)使用的注意

因爲String類有常量池,當使用synchronized(string)同步代碼塊的時候,會出現同步阻塞,大多數情況不會使用String作爲鎖對象

5)synchronized使用基本原則

第一條:當一個線程訪問“某對象”的“synchronized方法”或者“synchronized代碼塊”時,其他線程對“該對象”的該“synchronized方法”或者“synchronized代碼塊”的訪問將被阻塞。
           第二條:當一個線程訪問“某對象”的“synchronized方法”或者“synchronized代碼塊”時,其他線程仍然可以訪問“該對象”的非同步代碼塊
           第三條:當一個線程訪問“某對象”的“synchronized方法”或者“synchronized代碼塊”時,其他線程對“該對象”的其他的“synchronized方法”或者“synchronized代碼塊”的訪問將被阻塞。

 

四、synchronized實現原理及應用

從Java虛擬機規範中我們可以看到synchronized的實現原理,Java虛擬機支持方法和指令序列的同步通過一個單一的同步構造,monitor監視器對象。但是同步方法和同步代碼塊這兩者的實現細節是不一樣的,儘管同步方法使用monitorenter和monitorexit指令同樣達到與同步代碼塊一樣的效果。

1)同步方法實現原理

方法級別的同步是隱式的,同步方法在運行時常量池中的method_info結構中通過一個被方法調用指令檢查的ACC_SYNCRONIZED標誌來區分。當調用一個被ACC_SYNCHRONIZED標誌設置的方法時,正在執行的線程會進入到一個monitor,即獲取到對象的monitor所有權,當方法調用正常完成或者突然中斷會退出monitor,即釋放對象的monitor所有權。如果在執行同步方法時發生異常,在異常被重新拋出同步的方法外部之前會自動退出monitor,即釋放掉monitor所有權。

2)同步代碼塊實現原理

同步指令序列通常被用於編碼Java語言的同步代碼塊。通過使用monitorenter和monitorexit指令來實現。

我們來先看一下monitorenter,直接看一下JVM規範描述:

Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
• If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
• If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
• If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.

意思就是:每一個對象都關聯一個monitor,即對象監視器

  • 如果對象引用關聯的monitor進入數爲0,線程進入monitor並將進入數設爲1,此線程就是monitor所有權擁有者
  • 如果線程已經擁有了對象引用關聯的monitor,就會重新進入monitor,將進入數加1,這就是synchronized鎖的可重入特性
  • 如果另一個線程已經擁有了對象引用關聯的monitor,線程阻塞直到monitor監視器的進入數爲0,然後嘗試重新獲取monitor所有權
public class ThreadDemo extends Thread {
    private Service1 service1;
    public ThreadDemo(Service1 service1){
        this.service1 =service1;
    }
    @Override
    public void run() {
        service1.getName();
    }
    public static void main(String[] args) {
        Service1 service1 = new Service1();
        ThreadDemo thread = new ThreadDemo(service1);
        thread.start();

    }
}

class Service1{
    public void getName(){
        synchronized (this){
            System.out.println("lalala");
            //當前實例已經擁有了monitor所有權,當再次調用其他同步塊代碼時,會再次獲得鎖
            //synchronized可重入鎖功能
            getAge();
        }
    }
    public void getAge(){
        synchronized (this){
            System.out.println("hahaha");
        }
    }
}

我們通過命令:javap -v Service1.class 查看Service1.class的class指令,如下圖:

從上圖可以看出,執行getAge()方法時只有一個monitorexit指令,並沒有monitorenter指令,這就是鎖的可重入性,當一個線程獲得了某個對象的鎖後,此時這個對象的鎖還沒有被釋放,當其再次想獲得這個對象的鎖時還是可以獲取的。當我們執行getAge()方法的同步代碼塊時,我們會重新進入monitor將進入數加1,所以,雖然一個monitorenter對應兩個monitorexit,但是最後全部執行完,進入數最終是減爲0的,即釋放鎖monitor對象的所有權。

下圖(摘自《Java併發編程的藝術》一書)表現了對象,對象監視器,同步隊列以及執行線程狀態之間的關係:

 

從圖中可以看到,任意線程對Object(Object由synchronized保護)的訪問,首先要獲得Object的監視器。如果獲取失敗,線程進入同步隊列,線程狀態變爲BLOCKED。當訪問Object的前驅(獲得了鎖的線程)釋放了鎖,則該釋放操作喚醒阻塞在同步隊列中的線程,使其重新嘗試對監視器的獲取。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章