首頁 > 軟體

Java synchronized同步方法詳解

2022-03-02 13:01:00

面試題:

1.如何保證多執行緒下 i++ 結果正確?

2.一個執行緒如果出現了執行時異常會怎麼樣?

3.一個執行緒執行時發生異常會怎樣?

為了避免臨界區的競態條件發生,有多種手段可以達到目的。

(1) 阻塞式的解決方案:synchronized,Lock

(2) 非阻塞式的解決方案:原子變數

synchronized 即俗稱的【物件鎖】,它採用互斥的方式讓同一 時刻至多隻有一個執行緒能持有【物件鎖】,其它執行緒再想獲取這個【物件鎖】時就會阻塞住。這樣就能保證擁有鎖 的執行緒可以安全的執行臨界區內的程式碼,不用擔心執行緒上下文切換。

1. synchronized 同步方法

當使用synchronized關鍵字修飾一個方法的時候,該方法被宣告為同步方法,關鍵字synchronized的位置處於同步方法的返回型別之前。

public class SafeDemo {
    // 臨界區資源
    private static int i = 0;

    // 臨界區程式碼
    public void selfIncrement(){
        for(int j=0;j<5000;j++){
            i++;
        }
    }

    public int getI(){
        return i;
    }
}
public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        SafeDemo safeDemo = new SafeDemo();
        // 執行緒1和執行緒2同時執行臨界區程式碼段
        Thread t1 = new Thread(()->{
            safeDemo.selfIncrement();
        });
        Thread t2 = new Thread(()->{
            safeDemo.selfIncrement();
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(safeDemo.getI()); // 9906
    }
}

可以發現,當2個執行緒同時存取臨界區的selfIncrement()方法時,就會出現競態條件的問題,即2個執行緒在臨界區程式碼段的並行執行結果因為程式碼的執行順序不同而導致結果無法預測,每次執行都會得到不一樣的結果。因此,為了避免競態條件的問題,我們必須保證臨界區程式碼段操作具備排他性。這就意味著當一個執行緒進入臨界區程式碼段執行時,其他執行緒不能進入臨界區程式碼段執行。

現在使用synchronized關鍵字對臨界區程式碼段進行保護,程式碼如下:

public class SafeDemo {
    // 臨界區資源
    private static int i = 0;

    // 臨界區程式碼使用synchronized關鍵字進行保護
    public synchronized void selfIncrement(){
        for(int j=0;j<5000;j++){
            i++;
        }
    }

    public int getI(){
        return i;
    }
}

經過多次執行測試用例程式,累加10000次之後,最終的結果不再有偏差,與預期的結果(10000)是相同的。

在方法宣告中設定synchronized同步關鍵字,保證其方法的程式碼執行流程是排他性的。任何時間只允許一個執行緒進入同步方法(臨界區程式碼段),如果其他執行緒需要執行同一個方法,那麼只能等待和排隊。

2. synchronized 方法將物件作為鎖

定義執行緒的執行邏輯:

public class ThreadTask {
	
    // 臨界區程式碼使用synchronized關鍵字進行保護
    public synchronized void test() {
        try {
            System.out.println(Thread.currentThread().getName()+" begin");
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName()+" end");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

分別建立兩個執行緒,在兩個執行緒的執行體中執行執行緒邏輯:

public class ThreadA extends Thread {

    ThreadTask threadTask ;

    public ThreadA(ThreadTask threadTask){
        super();
        this.threadTask = threadTask;
    }

    @Override
    public void run() {
        threadTask.test();
    }
}
public class ThreadB extends Thread {
    ThreadTask threadTask ;

    public ThreadB(ThreadTask threadTask){
        super();
        this.threadTask = threadTask;
    }

    @Override
    public void run() {
        threadTask.test();
    }
}

建立一個鎖物件,傳給兩個執行緒:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        ThreadTask threadTask = new ThreadTask();
        ThreadA t1 = new ThreadA(threadTask);
        ThreadB t2 = new ThreadB(threadTask);
        t1.start();
        t2.start();
    }
}

執行結果:

Thread-0 begin
Thread-0 end
Thread-1 begin
Thread-1 end

這裡兩個執行緒的鎖物件都是threadTask,所以同一時間只有一個執行緒能拿到這個鎖物件,執行同步程式碼塊。另外,需要牢牢記住“共用”這兩個字,只有共用資源的寫存取才需要同步化,如果不是共用資源,那麼就沒有同步的必要。

總結:

(1) A執行緒先持有object物件的鎖,B執行緒如果在這時呼叫object物件中的synchronized型別的方法,則需等待,也就是同步;

(2) 在方法宣告處新增synchronized並不是鎖方法,而是鎖當前類的物件;

(3) 在Java中只有將物件作為鎖,並沒有鎖方法這種說法;

(4) 在Java語言中,鎖就是物件,物件可以對映成鎖,哪個執行緒拿到這把鎖,哪個執行緒就可以執行這個物件中的synchronized同步方法;

(5) 如果在X物件中使用了synchronized關鍵字宣告非靜態方法,則X物件就被當成鎖;

3. 多個鎖物件

建立兩個執行緒執行邏輯ThreadTask物件,即產生了兩把鎖

public class Main {
    public static void main(String[] args) throws InterruptedException {
        ThreadTask threadTask1 = new ThreadTask();
        ThreadTask threadTask2 = new ThreadTask();
        // 兩個執行緒分別執行兩個不同的執行緒執行邏輯物件
        ThreadA t1 = new ThreadA(threadTask1);
        ThreadB t2 = new ThreadB(threadTask2);
        t1.start();
        t2.start();
    }
}

執行結果:

Thread-0 begin
Thread-1 begin
Thread-0 end
Thread-1 end

test()方法使用了synchronized關鍵字,任何時間只允許一個執行緒進入同步方法,如果其他執行緒需要執行同一個方法,那麼只能等待和排隊。執行結果呈現了兩個執行緒交叉輸出的效果,說明兩個執行緒以非同步方式同時執行。

在系統中產生了兩個鎖,ThreadA的鎖物件是threadTask1,ThreadB的鎖物件是threadTas2,執行緒和業務物件屬於一對一的關係,每個執行緒執行自己所屬業務物件中的同步方法,不存在鎖的爭搶關係,所以執行結果是非同步的。

synchronized方法的同步鎖實質上使用了this物件鎖,哪個執行緒先執行帶synchronized關鍵字的方法,哪個執行緒就持有該方法所屬物件作為鎖(哪個物件呼叫了帶有synchronized關鍵字的方法,哪個物件就是鎖),其他執行緒只能等待,前提是多個執行緒存取的是同一個物件。

4. 如果同步方法內的執行緒丟擲異常會發生什麼?

public class SafeDemo {
    public synchronized void selfIncrement(){
        if(Thread.currentThread().getName().equals("t1")){
            System.out.println("t1 執行緒正在執行");
            int a=1;
            // 死迴圈,只要t1執行緒沒有執行完這個方法,就不會釋放鎖
            while (a==1){
                
            }
        }else{
            System.out.println("t2 執行緒正在執行");
        }
    }
}
public class SafeDemo {
    public synchronized void selfIncrement(){
        if(Thread.currentThread().getName().equals("t1")){
            System.out.println("t1 執行緒正在執行");
            int a=1;
            while (a==1){
                Integer.parseInt("a");
            }
        }else{
            System.out.println("t2 執行緒正在執行");
        }
    }
}

執行結果:t2執行緒得不到執行

t1 執行緒正在執行

此時,如果我們在同步方法中製造一個異常:

public class SafeDemo {
    public synchronized void selfIncrement(){
        if(Thread.currentThread().getName().equals("t1")){
            System.out.println("t1 執行緒正在執行");
            int a=1;
            while (a==1){
                Integer.parseInt("a");
            }
        }else{
            System.out.println("t2 執行緒正在執行");
        }
    }
}

執行緒t1出現異常並釋放鎖,執行緒t2進入方法正常輸出,說明出現異常時,鎖被自動釋放了。

5. 靜態的同步方法

在Java世界裡一切皆物件。Java有兩種物件:Object範例物件和Class物件。每個類執行時的型別資訊用Class物件表示,它包含與類名稱、繼承關係、欄位、方法有關的資訊。JVM將一個類載入入自己的方法區記憶體時,會為其建立一個Class物件,對於一個類來說其Class物件是唯一的。Class類沒有公共的構造方法,Class物件是在類載入的時候由Java虛擬機器器呼叫類載入器中的defineClass方法自動構造的,因此不能顯式地宣告一個Class物件。

普通的synchronized實體方法,其同步鎖是當前物件this的監視鎖。如果某個synchronized方法是static(靜態)方法,而不是普通的物件實體方法,其同步鎖又是什麼呢?

public class StaticSafe {
    // 臨界資源
    private static int count = 0;
    // 使用synchronized關鍵字修飾static方法
    public static synchronized void test(){
        count++;
    }
}

靜態方法屬於Class範例而不是單個Object範例,在靜態方法內部是不可以存取Object範例的this參照的。所以,修飾static方法的synchronized關鍵字就沒有辦法獲得Object範例的this物件的監視鎖。

實際上,使用synchronized關鍵字修飾static方法時,synchronized的同步鎖並不是普通Object物件的監視鎖,而是類所對應的Class物件的監視鎖。

為了以示區分,這裡將Object物件的監視鎖叫作物件鎖,將Class物件的監視鎖叫作類鎖。當synchronized關鍵字修飾static方法時,同步鎖為類鎖;當synchronized關鍵字修飾普通的成員方法時,同步鎖為物件鎖。由於類的物件範例可以有很多,但是每個類只有一個Class範例,因此使用類鎖作為synchronized的同步鎖時會造成同一個JVM內的所有執行緒只能互斥地進入臨界區段。

public class StaticSafe {
    // 臨界資源
    private static int count = 0;
    // 對JVM內的所有執行緒同步
    public static synchronized void test(){
        count++;
    }
}
z'z'z'z'z'z'z'z'z'z'z'z'z'z'z'z'z'z'z

所以,使用synchronized關鍵字修飾static方法是非常粗粒度的同步機制。

通過synchronized關鍵字所搶佔的同步鎖什麼時候釋放呢?一種場景是synchronized塊(程式碼塊或者方法)正確執行完畢,監視鎖自動釋放;另一種場景是程式出現異常,非正常退出synchronized塊,監視鎖也會自動釋放。所以,使用synchronized塊時不必擔心監視鎖的釋放問題。

總結

本篇文章就到這裡了,希望能夠給你帶來幫助,也希望您能夠多多關注it145.com的更多內容!  


IT145.com E-mail:sddin#qq.com