Java多線程之synchronized詳解


一、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.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM