死磕 java同步系列之synchronized解析

問題

(1)synchronized的特性?編程

(2)synchronized的實現原理?緩存

(3)synchronized是否可重入?併發

(4)synchronized是不是公平鎖?性能

(5)synchronized的優化?學習

(6)synchronized的五種使用方式?優化

簡介

synchronized關鍵字是Java裏面最基本的同步手段,它通過編譯以後,會在同步塊的先後分別生成 monitorenter 和 monitorexit 字節碼指令,這兩個字節碼指令都須要一個引用類型的參數來指明要鎖定和解鎖的對象。this

實現原理

在學習Java內存模型的時候,咱們介紹過兩個指令:lock 和 unlock。spa

lock,鎖定,做用於主內存的變量,它把主內存中的變量標識爲一條線程獨佔狀態。線程

unlock,解鎖,做用於主內存的變量,它把鎖定的變量釋放出來,釋放出來的變量才能夠被其它線程鎖定。code

可是這兩個指令並無直接提供給用戶使用,而是提供了兩個更高層次的指令 monitorenter 和 monitorexit 來隱式地使用 lock 和 unlock 指令。

而 synchronized 就是使用 monitorenter 和 monitorexit 這兩個指令來實現的。

根據JVM規範的要求,在執行monitorenter指令的時候,首先要去嘗試獲取對象的鎖,若是這個對象沒有被鎖定,或者當前線程已經擁有了這個對象的鎖,就把鎖的計數器加1,相應地,在執行monitorexit的時候會把計數器減1,當計數器減少爲0時,鎖就釋放了。

咱們仍是來上一段代碼,看看編譯後的字節碼長啥樣來學習:

public class SynchronizedTest {

    public static void sync() {
        synchronized (SynchronizedTest.class) {
            synchronized (SynchronizedTest.class) {
            }
        }
    }

    public static void main(String[] args) {

    }
}

咱們這段代碼很簡單,只是簡單地對SynchronizedTest.class對象加了兩次synchronized,除此以外,啥也沒幹。

編譯後的sync()方法的字節碼指令以下,爲了便於閱讀,彤哥特地加上了註釋:

// 加載常量池中的SynchronizedTest類對象到操做數棧中
0 ldc #2 <com coolcoding code synchronize synchronizedtest>
// 複製棧頂元素
2 dup
// 存儲一個引用到本地變量0中,後面的0表示第幾個變量
3 astore_0
// 調用monitorenter,它的參數變量0,也就是上面的SynchronizedTest類對象
4 monitorenter
// 再次加載常量池中的SynchronizedTest類對象到操做數棧中
5 ldc #2 <com coolcoding code synchronize synchronizedtest>
// 複製棧頂元素
7 dup
// 存儲一個引用到本地變量1中
8 astore_1
// 再次調用monitorenter,它的參數是變量1,也仍是SynchronizedTest類對象
9 monitorenter
// 從本地變量表中加載第1個變量
10 aload_1
// 調用monitorexit解鎖,它的參數是上面加載的變量1
11 monitorexit
// 跳到第20行
12 goto 20 (+8)
15 astore_2
16 aload_1
17 monitorexit
18 aload_2
19 athrow
// 從本地變量表中加載第0個變量
20 aload_0
// 調用monitorexit解鎖,它的參數是上面加載的變量0
21 monitorexit
// 跳到第30行
22 goto 30 (+8)
25 astore_3
26 aload_0
27 monitorexit
28 aload_3
29 athrow
// 方法返回,結束
30 return

按照彤哥的註釋讀起來,字節碼比較簡單,咱們的synchronized鎖定的是SynchronizedTest類對象,能夠看到它從常量池中加載了兩次SynchronizedTest類對象,分別存儲在本地變量0和本地變量1中,解鎖的時候正好是相反的順序,先解鎖變量1,再解鎖變量0,實際上變量0和變量1指向的是同一個對象,因此synchronized是可重入的。

至於,被加鎖的對象具體在對象頭中是怎麼存儲的,彤哥這裏就不細講了,有興趣的能夠看看《Java併發編程的藝術》這本書。

公衆號後臺回覆「JMM」可領取這本書籍的pdf版。

原子性、可見性、有序性

前面講解Java內存模型的時候咱們說過內存模型主要就是用來解決緩存一致性的問題的,而緩存一致性主要包括原子性、可見性、有序性。

那麼,synchronized關鍵字可否保證這三個特性呢?

仍是回到Java內存模型上來,synchronized關鍵字底層是經過monitorenter和monitorexit實現的,而這兩個指令又是經過lock和unlock來實現的。

而lock和unlock在Java內存模型中是必須知足下面四條規則的:

(1)一個變量同一時刻只容許一條線程對其進行lock操做,但lock操做能夠被同一個線程執行屢次,屢次執行lock後,只有執行相同次數的unlock操做,變量才能被解鎖。

(2)若是對一個變量執行lock操做,將會清空工做內存中此變量的值,在執行引擎使用這個變量前,須要從新執行load或assign操做初始化變量的值;

(3)若是一個變量沒有被lock操做鎖定,則不容許對其執行unlock操做,也不容許unlock一個其它線程鎖定的變量;

(4)對一個變量執行unlock操做以前,必須先把此變量同步回主內存中,即執行store和write操做;

經過規則(1),咱們知道對於lock和unlock之間的代碼,同一時刻只容許一個線程訪問,因此,synchronized是具備原子性的。

經過規則(1)(2)和(4),咱們知道每次lock和unlock時都會從主內存加載變量或把變量刷新回主內存,而lock和unlock之間的變量(這裏是指鎖定的變量)是不會被其它線程修改的,因此,synchronized是具備可見性的。

經過規則(1)和(3),咱們知道全部對變量的加鎖都要排隊進行,且其它線程不容許解鎖當前線程鎖定的對象,因此,synchronized是具備有序性的。

綜上所述,synchronized是能夠保證原子性、可見性和有序性的。

公平鎖 VS 非公平鎖

經過上面的學習,咱們知道了synchronized的實現原理,而且它是可重入的,那麼,它是不是公平鎖呢?

直接上菜:

public class SynchronizedTest {

    public static void sync(String tips) {
        synchronized (SynchronizedTest.class) {
            System.out.println(tips);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(()-&gt;sync("線程1")).start();
        Thread.sleep(100);
        new Thread(()-&gt;sync("線程2")).start();
        Thread.sleep(100);
        new Thread(()-&gt;sync("線程3")).start();
        Thread.sleep(100);
        new Thread(()-&gt;sync("線程4")).start();
    }
}

在這段程序中,咱們起了四個線程,且分別間隔100ms啓動,每一個線程裏面打印一句話後等待1000ms,若是synchronized是公平鎖,那麼打印的結果應該依次是 線程一、二、三、4。

可是,實際運行的結果幾乎不會出現上面的樣子,因此,synchronized是一個非公平鎖。

鎖優化

Java在不斷進化,一樣地,Java中像synchronized這種古老的東西也在不斷進化,好比ConcurrentHashMap在jdk7的時候仍是使用ReentrantLock加鎖的,在jdk8的時候已經換成了原生的synchronized了,可見synchronized有原生的支持,它的進化空間仍是很大的。

那麼,synchronized有哪些進化中的狀態呢?

咱們這裏稍作一些簡單地介紹:

(1)偏向鎖,是指一段同步代碼一直被一個線程訪問,那麼這個線程會自動獲取鎖,下降獲取鎖的代價。

(2)輕量級鎖,是指當鎖是偏向鎖時,被另外一個線程所訪問,偏向鎖會升級爲輕量級鎖,這個線程會經過自旋的方式嘗試獲取鎖,不會阻塞,提升性能。

(3)重量級鎖,是指當鎖是輕量級鎖時,當自旋的線程自旋了必定的次數後,尚未獲取到鎖,就會進入阻塞狀態,該鎖升級爲重量級鎖,重量級鎖會使其餘線程阻塞,性能下降。

總結

(1)synchronized在編譯時會在同步塊先後生成monitorenter和monitorexit字節碼指令;

(2)monitorenter和monitorexit字節碼指令須要一個引用類型的參數,基本類型不能夠哦;

(3)monitorenter和monitorexit字節碼指令更底層是使用Java內存模型的lock和unlock指令;

(4)synchronized是可重入鎖;

(5)synchronized是非公平鎖;

(6)synchronized能夠同時保證原子性、可見性、有序性;

(7)synchronized有三種狀態:偏向鎖、輕量級鎖、重量級鎖;

彩蛋——synchronized的五種使用方式

經過上面的分析,咱們知道synchronized是須要一個引用類型的參數的,而這個引用類型的參數在Java中其實能夠分紅三大類:類對象、實例對象、普通引用,使用方式分別以下:

public class SynchronizedTest2 {

    public static final Object lock = new Object();

    // 鎖的是SynchronizedTest.class對象
    public static synchronized void sync1() {

    }

    public static void sync2() {
        // 鎖的是SynchronizedTest.class對象
        synchronized (SynchronizedTest.class) {

        }
    }

    // 鎖的是當前實例this
    public synchronized void sync3() {

    }

    public void sync4() {
        // 鎖的是當前實例this
        synchronized (this) {

        }
    }

    public void sync5() {
        // 鎖的是指定對象lock
        synchronized (lock) {

        }
    }
}

在方法上使用synchronized的時候要注意,會隱式傳參,分爲靜態方法和非靜態方法,靜態方法上的隱式參數爲當前類對象,非靜態方法上的隱式參數爲當前實例this。

另外,多個synchronized只有鎖的是同一個對象,它們之間的代碼纔是同步的,這一點在使用synchronized的時候必定要注意。

相關文章
相關標籤/搜索