深入詳細瞭解synchronized底層原理

小猴子的技術筆記發表於2020-12-15

    歡迎大家搜尋“小猴子的技術筆記”關注我的公眾號,有問題可以及時和我交流。


    在多執行緒之間,共享變數的值是執行緒不安全的,因為執行緒在開始執行之後都會擁有自己的工作空間,而從自己工作空間把修改的值重新整理回主存的時候需要CPU的排程。因此,一個執行緒看到的變數可能並不是最新的。

    我們假設有個Share類中存放了一個共享的變數“count”。

public class Share {
    public int count = 10000;
    public void decrement() {
        count--;
    }
    public int getCount() {
        return count;
    }
}

    然後有兩個執行緒可以對這個共享的變數進行操作,每個執行緒都呼叫了5000次“decrement()”方法類進行共享變數的值修改:

public class ShareThread01 implements Runnable{
    private Share share;
    public AccountThread01(Share share) {
        this.share = share;
    }
    @Override
    public void run() {
        for (int i = 0; i < 5000; i++) {
            share.decrement();
        }
    }
}
public class ShareThread02 implements Runnable{
    private Share share;
    public AccountThread02(Share share) {
        this.share = share;
    }
    @Override
    public void run() {
        for (int i = 0; i < 5000; i++) {
            share.decrement();
        }
    }
}

    如果上面的程式碼按照預期的執行,那麼最後的結果應該是0。請執行下面的程式碼進行驗證:

public class ShareTest {
    public static void main(String[] args) throws InterruptedException {
       while(true) {
           Share share = new Share();
           Thread t1 = new Thread(new ShareThread01(share));
           Thread t2 = new Thread(new ShareThread02(share));
           t1.start();
           t2.start();
           TimeUnit.SECONDS.sleep(2);
           System.out.println(share.getCount());
       }
    }
}

    執行上面的程式,你會發現每次輸出的結果是不一樣的。因為文章開頭已經說過,這是由於Java在多個執行緒同時訪問同一個物件的成員變數的時候,每個執行緒都擁有了這個物件變數的拷貝。因此在程式執行的過程中,一個執行緒所看到的變數並不一定是最新的。

在這裡插入圖片描述
    也許你想到了使用之前學習的“volatile”關鍵字來使共享變數進行記憶體可見,保證執行緒安全。於是上述程式改成了如下:

public class ShareThread02 implements Runnable{
    private volatile Share share;
    public ShareThread02(Share share) {
        this.share = share;
    }
    @Override
    public void run() {
        for (int i = 0; i < 5000; i++) {
            share.decrement();
        }
    }
}

    然後再次執行測試程式也會發現,所輸出的結果並不是你想要的結果。請記住:關鍵字“volatile”只是保證了多執行緒之間共享變數的記憶體可見性,它並不保證共享變數的原子性。

    這個時候關鍵字“synchronized”就派上了用場。它可以保證,同一個時刻只有一個執行緒能夠訪問被“synchronized”修飾的方法或者程式碼塊。將上述程式碼改成下面這樣:

public class Share {
    public int count = 10000;
    public synchronized void decrement() {
        count--;
    }
    public int getCount() {
        return count;
    }
}

    再次執行測試程式,這個時候你會發現,每次得到的結果都是一樣的。那麼為什麼新增了關鍵字“synchronized”之後就能夠按照預期的進行執行呢?

    通過執行“javap -v Share.class”來看看底層做了哪些修飾:
在這裡插入圖片描述
    可以看到,在方法上進行了關鍵字”synchronized“的修飾,底層的實現是標記了一個"ACC_SYNCHRONIZED"的標識。程式碼如果遇到了這個標識,就表示獲取到了物件的監視器monitor(monitor物件是由C++實現的),這個獲取的過程是排他的,也就是同一時刻只能有一個執行緒獲取到由synchronized所保護物件的監視器。

    除此之外,關鍵字”synchronized“還可以對程式碼塊進行加鎖:

public class Share {
    public int count = 10000;
    public void decrement() {
        synchronized (Share.class) {
            count--;
        }
    }
    public int getCount() {
        return count;
    }
}

    將上述程式碼執行“javap -v Share.class”反編譯之後,可以看到如下:
在這裡插入圖片描述
    ”synchronized“關鍵字鎖程式碼塊的時候他提供了“monitor enter”和“monitor exit”兩個JVM指令,它能夠保證任何時候執行緒執行到“monitor enter”成功之前都必須從主記憶體中獲取資料。“monitor exit”退出之後,共享變數被更新後的值重新整理到主記憶體中,因此”synchronized“關鍵字還可以保證記憶體的可見性。

    如果你仔細觀察就會發現,15和21有兩個“monitor exit”那麼為什麼會有兩個“monitor exit”呢?我們做這樣一個假設:如果執行緒啟動執行的過程中突然遇到異常了,這個時候執行緒該怎麼辦呢?總不能一直持有鎖吧!於是執行緒就會釋放鎖。因此,第二個“monitor exit”是為了執行緒遇到異常之後釋放鎖而準備的。
在這裡插入圖片描述

相關文章