1. 程式人生 > >死磕 java同步系列之synchronized解析

死磕 java同步系列之synchronized解析

問題

(1)synchronized的特性?

(2)synchronized的實現原理?

(3)synchronized是否可重入?

(4)synchronized是否是公平鎖?

(5)synchronized的優化?

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

簡介

synchronized關鍵字是Java裡面最基本的同步手段,它經過編譯之後,會在同步塊的前後分別生成 monitorenter 和 monitorexit 位元組碼指令,這兩個位元組碼指令都需要一個引用型別的引數來指明要鎖定和解鎖的物件。

實現原理

在學習Java記憶體模型的時候,我們介紹過兩個指令:lock 和 unlock。

lock,鎖定,作用於主記憶體的變數,它把主記憶體中的變數標識為一條執行緒獨佔狀態。

unlock,解鎖,作用於主記憶體的變數,它把鎖定的變數釋放出來,釋放出來的變數才可以被其它執行緒鎖定。

但是這兩個指令並沒有直接提供給使用者使用,而是提供了兩個更高層次的指令 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(()->sync("執行緒1")).start();
        Thread.sleep(100);
        new Thread(()->sync("執行緒2")).start();
        Thread.sleep(100);
        new Thread(()->sync("執行緒3")).start();
        Thread.sleep(100);
        new Thread(()->sync("執行緒4")).start();
    }
}

在這段程式中,我們起了四個執行緒,且分別間隔100ms啟動,每個執行緒裡面列印一句話後等待1000ms,如果synchronized是公平鎖,那麼列印的結果應該依次是 執行緒1、2、3、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的時候一定要注意。

推薦閱讀

  1. 死磕 java同步系列之JMM(Java Memory Model)

  2. 死磕 java同步系列之volatile解析


歡迎關注我的公眾號“彤哥讀原始碼”,檢視更多原始碼系列文章, 與彤哥一起暢遊原始碼的海洋。

相關推薦

java同步系列synchronized解析

問題 (1)synchronized的特性? (2)synchronized的實現原理? (3)synchronized是否可重入? (4)synchronized是否是公平鎖? (5)synchronized的優化? (6)synchronized的五種使用方式? 簡介 synchronized關鍵字是Ja

java同步系列volatile解析

問題 (1)volatile是如何保證可見性的? (2)volatile是如何禁止重排序的? (3)volatile的實現原理? (4)volatile的缺陷? 簡介 volatile可以說是Java虛擬機器提供的最輕量級的同步機制了,但是它並不容易被正確地理解,以至於很多人不習慣使用它,遇到多執行緒問題一律

java同步系列ReentrantLock原始碼解析(一)——公平鎖、非公平鎖

問題 (1)重入鎖是什麼? (2)ReentrantLock如何實現重入鎖? (3)ReentrantLock為什麼預設是非公平模式? (4)ReentrantLock除了可重入還有哪些特性? 簡介 Reentrant = Re + entrant,Re是重複、又、再的意思,entrant是enter的名詞或

java同步系列ReentrantLock原始碼解析(二)——條件鎖

問題 (1)條件鎖是什麼? (2)條件鎖適用於什麼場景? (3)條件鎖的await()是在其它執行緒signal()的時候喚醒的嗎? 簡介 條件鎖,是指在獲取鎖之後發現當前業務場景自己無法處理,而需要等待某個條件的出現才可以繼續處理時使用的一種鎖。 比如,在阻塞佇列中,當佇列中沒有元素的時候是無法彈出一個元素

java同步系列ReentrantLock VS synchronized——結果可能跟你想的不一樣

問題 (1)ReentrantLock有哪些優點? (2)ReentrantLock有哪些缺點? (3)ReentrantLock

java同步系列ReentrantReadWriteLock原始碼解析

問題 (1)讀寫鎖是什麼? (2)讀寫鎖具有哪些特性? (3)ReentrantReadWriteLock是怎麼實現讀寫鎖的? (4)如何使用ReentrantReadWriteLock實現高效安全的TreeMap? 簡介 讀寫鎖是一種特殊的鎖,它把對共享資源的訪問分為讀訪問和寫訪問,多個執行緒可以同時對共享

java同步系列Semaphore原始碼解析

問題 (1)Semaphore是什麼? (2)Semaphore具有哪些特性? (3)Semaphore通常使用在什麼場景中? (

java同步系列StampedLock原始碼解析

問題 (1)StampedLock是什麼? (2)StampedLock具有什麼特性? (3)StampedLock是否支援可重入

java同步系列CyclicBarrier原始碼解析——有圖有真相

問題 (1)CyclicBarrier是什麼? (2)CyclicBarrier具有什麼特性? (3)CyclicBarrier與

java同步系列Phaser原始碼解析

問題 (1)Phaser是什麼? (2)Phaser具有哪些特性? (3)Phaser相對於CyclicBarrier和Count

java同步系列開篇

討論 關註 使用 避免死鎖 更新數據 讀寫 上下文切換 monit 缺點 簡介 同步系列,這是彤哥想了好久的名字,本來是準備寫鎖相關的內容,但是java中的CountDownLatch、Semaphore、CyclicBarrier這些類又不屬於鎖,它們和鎖又有很多共同點,

java同步系列JMM(Java Memory Model)

簡介 Java記憶體模型是在硬體記憶體模型上的更高層的抽象,它遮蔽了各種硬體和作業系統訪問的差異性,保證了Java程式在各種平臺下對記憶體的訪問都能達到一致的效果。 硬體記憶體模型 在正式講解Java的記憶體模型之前,我們有必要先了解一下硬體層面的一些東西。 在現代計算機的硬體體系中,CPU的運算速度是非常快

java同步系列自己動手寫一個鎖Lock

問題 (1)自己動手寫一個鎖需要哪些知識? (2)自己動手寫一個鎖到底有多簡單? (3)自己能不能寫出來一個完美的鎖? 簡介 本篇文章的目標一是自己動手寫一個鎖,這個鎖的功能很簡單,能進行正常的加鎖、解鎖操作。 本篇文章的目標二是通過自己動手寫一個鎖,能更好地理解後面章節將要學習的AQS及各種同步器實現的原理

java同步系列AQS起篇

問題 (1)AQS是什麼? (2)AQS的定位? (3)AQS的實現原理? (4)基於AQS實現自己的鎖? 簡介 AQS的全稱是AbstractQueuedSynchronizer,它的定位是為Java中幾乎所有的鎖和同步器提供一個基礎框架。 AQS是基於FIFO的佇列實現的,並且內部維護了一個狀態變數sta

java同步系列AQS終篇(面試)

問題 (1)AQS的定位? (2)AQS的重要組成部分? (3)AQS運用的設計模式? (4)AQS的總體流程? 簡介 AQS的全稱是AbstractQueuedSynchronizer,它的定位是為Java中幾乎所有的鎖和同步器提供一個基礎框架。 在之前的章節中,我們一起學習了ReentrantLock、R

java同步系列mysql分散式鎖

問題 (1)什麼是分散式鎖? (2)為什麼需要分散式鎖? (3)mysql如何實現分散式鎖? (4)mysql分散式鎖的優點和缺點? 簡介 隨著併發量的不斷增加,單機的服務遲早要向多節點或者微服務進化,這時候原來單機模式下使用的synchronized或者ReentrantLock將不再適用,我們迫切地需要一

java同步系列zookeeper分散式鎖

(2)zookeeper分散式鎖有哪些優點? (3)zookeeper分散式鎖有哪些缺點? 簡介 zooKeeper是一個分散式的,開放原始碼的分散式應用程式協調服務,它可以為分散式應用提供一致性服務,它是Hadoop和Hbase的重要元件,同時也可以作為配置中心、註冊中心運用在微服務體系中。 本章我們將介

java同步系列redis分散式鎖進化史

(2)redis分散式鎖有哪些優點? (3)redis分散式鎖有哪些缺點? (4)redis實現分散式鎖有沒有現成的輪子可以使用? 簡介 Redis(全稱:Remote Dictionary Server 遠端字典服務)是一個開源的使用ANSI C語言編寫、支援網路、可基於記憶體亦可持久化的日誌型、Key-

java同步系列終結篇

腦圖 下面是關於同步系列的一份腦圖,列舉了主要的知識點和問題點,看過本系列文章的同學可以根據腦圖自行回顧所學的內容,也可以作為面試前的準備。 如果有需要高清無碼原圖的同學,可以關注公眾號“彤哥讀原始碼”,回覆“sync”領取。 總結 所謂同步,就是保證多執行緒(包括多程序)對共享資源的讀寫能夠安全有效的執