Java多執行緒之synchronized詳解

LanceToBigData發表於2021-03-11

一、Synchronized概述

多個執行緒訪問同一個資源時,需要對該資源上鎖。即同時只允許一個執行緒訪問該資源。任何執行緒要執行synchronized裡的程式碼,都必須先拿到鎖。synchronized底層實現,JVM並沒有規定必須應該如何實現,Hotspot在物件頭上(64位)拿出2位來記錄該物件是不是被鎖定(markword),即鎖定的是某個物件。

1.1、Synchronized作用

1)確保執行緒互斥的訪問同步程式碼

在同一時間只允許一個執行緒持有某個物件鎖,通過這種特性來實現多執行緒中的協調機制,這樣在同一時間只有一個執行緒對需同步的程式碼塊(複合操作)進行訪問。互斥性我們也往往稱為操作的原子性。

2)保證共享變數的修改能夠及時可見

必須確保在鎖被釋放之前,對共享變數所做的修改,對於隨後獲得該鎖的另一個執行緒是可見的(即在獲得鎖時應獲得最新共享變數的值),否則另一個執行緒可能是在本地快取的某個副本上繼續操作從而引起不一致。

3)有效解決重排序問題。

二、Synchronized用法

2.1、概述

Synchronized的三種用法:修飾普通方法、修飾靜態方法、修飾程式碼塊

2.1.1、修飾普通(例項)方法

   //同步非靜態方法 ,當前執行緒的鎖便是例項物件methodName
    public synchronized  void methodName() {
        try {
            TimeUnit.SECONDS.sleep(2);
            System.out.println(Thread.currentThread().getName()+"   aaa");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

當一個執行緒正在訪問一個物件的 synchronized 例項方法,那麼其他執行緒不能訪問該物件的其他 synchronized 方法,畢竟一個物件只有一把鎖,當一個執行緒獲取了該物件的鎖之後,其他執行緒無法獲取該物件的鎖,所以無法訪問該物件的其他synchronized例項方法,但是其他執行緒還是可以訪問該例項物件的其他非synchronized方法,當然如果是一個執行緒 A 需要訪問例項物件 obj1 的 synchronized 方法 f1(當前物件鎖是obj1),另一個執行緒 B 需要訪問例項物件 obj2 的 synchronized 方法 f2(當前物件鎖是obj2),這樣是允許的,因為兩個例項物件鎖並不同相同。此時如果兩個執行緒運算元據並非共享的,執行緒安全是有保障的,遺憾的是如果兩個執行緒操作的是共享資料,那麼執行緒安全就有可能無法保證了。

2.1.2、修飾靜態方法

//同步靜態方法
    public synchronized static void methodName() {
        try {
            TimeUnit.SECONDS.sleep(2);
            System.out.println(Thread.currentThread().getName()+"   aaa");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(SynchronizedDemo::methodName).start();
        }
    }

當synchronized作用於靜態方法時,其鎖就是當前類的class物件鎖。由於靜態成員不專屬於任何一個例項物件,是類成員,因此通過class物件鎖可以控制靜態 成員的併發操作。需要注意的是如果一個執行緒A呼叫一個例項物件的非static synchronized方法,而執行緒B需要呼叫這個例項物件所屬類的靜態 synchronized方法,是允許的,不會發生互斥現象,因為訪問靜態 synchronized 方法佔用的鎖是當前類的class物件,而訪問非靜態 synchronized 方法佔用的鎖是當前例項物件鎖。

2.1.3、修飾程式碼塊

1)程式碼塊方式(this)

//修飾非靜態方法
    public void methodName() {
        //修飾程式碼塊,this=當前物件(誰呼叫就指待誰)
        synchronized (this) {
            try {
                TimeUnit.SECONDS.sleep(2);
                System.out.println(Thread.currentThread().getName() + "   aaa");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

synchronized(this|object) {}:在 Java 中,每個物件都會有一個 monitor 物件,這個物件其實就是 Java 物件的鎖,通常會被稱為“內建鎖”或“物件鎖”。類的物件可以有多個,所以每個物件有其獨立的物件鎖,互不干擾

2)程式碼塊方式(Class)

 //修飾非靜態方法
    public void methodName() {
        //修飾程式碼塊,使用Class類
        //使用ClassLoader 載入位元組碼的時候會向堆裡面存放Class類,所有的物件都對應唯一的Class類
        //SynchronizedDemo.class 這裡拿到的就是堆裡面的Class類,也就是所有的Class的物件都共同使用這個synchronized
        synchronized (SynchronizedDemo.class) {
            try {
                TimeUnit.SECONDS.sleep(2);
                System.out.println(Thread.currentThread().getName() + "   aaa");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

synchronized(類.class) {}:在 Java 中,針對每個類也有一個鎖,可以稱為“類鎖”,類鎖實際上是通過物件鎖實現的,即類的 Class 物件鎖。每個類只有一個 Class 物件,所以每個類只有一個類鎖

在 Java 中,每個物件都會有一個 monitor 物件,監視器。

  1. 某一執行緒佔有這個物件的時候,先monitor 的計數器是不是0,如果是0還沒有執行緒佔有,這個時候執行緒佔有這個物件,並且對這個物件的monitor+1;如果不為0,表示這個執行緒已經被其他執行緒佔有,這個執行緒等待。當執行緒釋放佔有權的時候,monitor-1;
  2. 同一執行緒可以對同一物件進行多次加鎖,+1,+1,重入性。

2.2、程式碼演示

2.2.1、沒有同步執行

public class SynchronizedTest {
    public void method1(){
        System.out.println("Method 1 start");
        try {
            System.out.println("Method 1 execute");
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 1 end");
    }

    public void method2(){
        System.out.println("Method 2 start");
        try {
            System.out.println("Method 2 execute");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 2 end");
    }

    public static void main(String[] args) {
        final SynchronizedTest test = new SynchronizedTest();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method1();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method2();
            }
        }).start();
    }
}

結果:

執行緒1和執行緒2同時進入執行狀態,執行緒2執行速度比執行緒1快,所以執行緒2先執行完成,這個過程中執行緒1和執行緒2是同時執行的。

Method 1 start
Method 1 execute
Method 2 start
Method 2 execute
Method 2 end
Method 1 end

2.2.2、對普通方法同步

public class SynchronizedTest {
    public synchronized void method1(){
        System.out.println("Method 1 start");
        try {
            System.out.println("Method 1 execute");
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 1 end");
    }

    public synchronized void method2(){
        System.out.println("Method 2 start");
        try {
            System.out.println("Method 2 execute");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 2 end");
    }

    public static void main(String[] args) {
        final SynchronizedTest test = new SynchronizedTest();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method1();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method2();
            }
        }).start();
    }
}

結果:

執行結果如下,跟程式碼段一比較,可以很明顯的看出,執行緒2需要等待執行緒1的method1執行完成才能開始執行method2方法。

Method 1 start
Method 1 execute
Method 1 end
Method 2 start
Method 2 execute
Method 2 end

2.2.3、靜態方法(類)同步

public class SynchronizedTest {
     public static synchronized void method1(){
         System.out.println("Method 1 start");
         try {
             System.out.println("Method 1 execute");
             Thread.sleep(3000);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         System.out.println("Method 1 end");
     }

     public static synchronized void method2(){
         System.out.println("Method 2 start");
         try {
             System.out.println("Method 2 execute");
             Thread.sleep(1000);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         System.out.println("Method 2 end");
     }

     public static void main(String[] args) {
         final SynchronizedTest test = new SynchronizedTest();
         final SynchronizedTest test2 = new SynchronizedTest();

         new Thread(new Runnable() {
             @Override
             public void run() {
                 test.method1();
             }
         }).start();

         new Thread(new Runnable() {
             @Override
             public void run() {
                 test2.method2();
             }
         }).start();
     }
 }

結果:

對靜態方法的同步本質上是對類的同步(靜態方法本質上是屬於類的方法,而不是物件上的方法),所以即使test和test2屬於不同的物件,但是它們都屬於SynchronizedTest類的例項,所以也只能順序的執行method1和method2,不能併發執行。

Method 1 start
Method 1 execute
Method 1 end
Method 2 start
Method 2 execute
Method 2 end

2.2.4、程式碼塊同步

public class SynchronizedTest {
    public void method1(){
        System.out.println("Method 1 start");
        try {
            synchronized (this) {
                System.out.println("Method 1 execute");
                Thread.sleep(3000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 1 end");
    }

    public void method2(){
        System.out.println("Method 2 start");
        try {
            synchronized (this) {
                System.out.println("Method 2 execute");
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 2 end");
    }

    public static void main(String[] args) {
        final SynchronizedTest test = new SynchronizedTest();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method1();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method2();
            }
        }).start();
    }
}

結果:

雖然執行緒1和執行緒2都進入了對應的方法開始執行,但是執行緒2在進入同步塊之前,需要等待執行緒1中同步塊執行完成。

Method 1 start
Method 1 execute
Method 2 start
Method 1 end
Method 2 execute
Method 2 end

2.2.5、執行結果分析

1、程式碼段2結果:

  雖然method1和method2是不同的方法,但是這兩個方法都進行了同步,並且是通過同一個物件去呼叫的,所以呼叫之前都需要先去競爭同一個物件上的鎖(monitor),也就只能互斥的獲取到鎖,因此,method1和method2只能順序的執行。

2、程式碼段3結果:

  雖然test和test2屬於不同物件,但是test和test2屬於同一個類的不同例項,由於method1和method2都屬於靜態同步方法,所以呼叫的時候需要獲取同一個類上monitor(每個類只對應一個class物件),所以也只能順序的執行。

3、程式碼段4結果:

  對於程式碼塊的同步實質上需要獲取Synchronized關鍵字後面括號中物件的monitor,由於這段程式碼中括號的內容都是this,而method1和method2又是通過同一的物件去呼叫的,所以進入同步塊之前需要去競爭同一個物件上的鎖,因此只能順序執行同步塊。

三、Synchronized原理

3.1、同步程式碼塊分析

1.通過反編譯下面的程式碼來看看Synchronized是如何實現對程式碼塊進行同步的

public class SynchronizedTest {
    public void method(){
        synchronized (this){
            System.out.println("test enter method 001 start");
        }
    }
}

反編譯結果:

2.JVM指令分析

monitorenter:互斥入口

monitorexit:互斥出口(monitorexit有兩個,一個是正常出口,一個是異常出口)

1)monitorenter

每個物件有一個監視器鎖(monitor)。當monitor被佔用時就會處於鎖定狀態,執行緒執行monitorenter指令時嘗試獲取monitor的所有權,過程如下:

​ 如果monitor的進入數為0,則該執行緒進入monitor,然後將進入數設定為1,該執行緒即為monitor的所有者。

​ 如果執行緒已經佔有該monitor,只是重新進入,則進入monitor的進入數加1.

​ 如果其他執行緒已經佔用了monitor,則該執行緒進入阻塞狀態,直到monitor的進入數為0,再重新嘗試獲取monitor的所有權。

2)monitorexit

執行monitorexit的執行緒必須是objectref所對應的monitor的所有者。

指令執行時,monitor的進入數減1,如果減1後進入數為0,那執行緒退出monitor,不再是這個monitor的所有者。其他被這個monitor阻塞的執行緒可以嘗試去獲取這個 monitor 的所有權。

通過這兩段描述,我們應該能很清楚的看出Synchronized的實現原理,Synchronized的語義底層是通過一個monitor的物件來完成,其實wait/notify等方法也依賴於monitor物件,這就是為什麼只有在同步的塊或者方法中才能呼叫wait/notify等方法,否則會丟擲java.lang.IllegalMonitorStateException的異常的原因。

3.2、同步方法分析

原始碼:

public class SynchronizedTest {
    public synchronized void method() {
        System.out.println("test enter method 001 start");

    }
}

反編譯結果:

從反編譯的結果來看,方法的同步並沒有通過指令monitorenter和monitorexit來完成(理論上其實也可以通過這兩條指令來實現),不過相對於普通方法,其常量池中多了ACC_SYNCHRONIZED標示符。JVM就是根據該標示符來實現方法的同步的

當方法呼叫時,呼叫指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設定,

如果設定了,執行執行緒將先獲取monitor,獲取成功之後才能執行方法體,方法執行完後再釋放monitor。

在方法執行期間,其他任何執行緒都無法再獲得同一個monitor物件。

其實本質上沒有區別,只是方法的同步是一種隱式的方式來實現,無需通過位元組碼來完成

3.2、Synchronized的可重入性

從互斥鎖的設計上來說,當一個執行緒試圖操作一個由其他執行緒持有的物件鎖的臨界資源時,將會處於阻塞狀態,但當一個執行緒再次請求自己持有物件鎖的臨界資源時,這種情況屬於重入鎖,請求將會成功,在java中synchronized是基於原子性的內部鎖機制,是可重入的,因此在一個執行緒呼叫synchronized方法的同時在其方法體內部呼叫該物件另一個synchronized方法,也就是說一個執行緒得到一個物件鎖後再次請求該物件鎖,是允許的,這就是synchronized的可重入性。

public class AccountingSync implements Runnable{
    static AccountingSync instance=new AccountingSync();
    static int i=0;
    static int j=0;
    @Override
    public void run() {
        for(int j=0;j<1000000;j++){
            //this,當前例項物件鎖
            synchronized(this){
                i++;
                increase();//synchronized的可重入性
            }
        }
    }
    public synchronized void increase(){
        j++;
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);
        t1.start();t2.start();
        t1.join();t2.join();
        System.out.println(i);
    }
}

正如程式碼所演示的,在獲取當前例項物件鎖後進入synchronized程式碼塊執行同步程式碼,並在程式碼塊中呼叫了當前例項物件的另外一個synchronized方法,再次請求當前例項鎖時,將被允許,進而執行方法體程式碼,這就是重入鎖最直接的體現,需要特別注意另外一種情況,當子類繼承父類時,子類也是可以通過可重入鎖呼叫父類的同步方法。注意由於synchronized是基於monitor實現的,因此每次重入,monitor中的計數器仍會加1.

相關文章