1. 程式人生 > 實用技巧 >Synchronized與 ReentrantLock

Synchronized與 ReentrantLock

synchronized

synchronized 可以用來修飾以下 3 個層面:

  • 修飾例項方法;
  • 修飾靜態類方法;
  • 修飾程式碼塊。

synchronized 修飾例項方法
在這裡插入圖片描述這種情況下的鎖物件是當前例項物件,因此只有同一個例項物件呼叫此方法才會產生互斥效果,不同例項物件之間不會有互斥效果。比如如下程式碼:
在這裡插入圖片描述上述程式碼,在不同的執行緒中呼叫的是不同物件的 printLog 方法,因此彼此之間不會有排斥。執行效果如下:
在這裡插入圖片描述
可以看出,兩個執行緒是互動執行的。

如果將程式碼進行如下修改,兩個執行緒呼叫同一個物件的 printLog 方法:
在這裡插入圖片描述則執行效果如下:
在這裡插入圖片描述可以看出:只有某一個執行緒中的程式碼執行完之後,才會呼叫另一個執行緒中的程式碼。也就是說此時兩個執行緒間是互斥的。

修飾靜態類方法

如果 synchronized 修飾的是靜態方法,則鎖物件是當前類的 Class 物件。因此即使在不同執行緒中呼叫不同例項物件,也會有互斥效果。

將 LagouSynchronizedMehtods 中的 printLog 修改為靜態方法,如下:
在這裡插入圖片描述執行後的列印效果如下:
在這裡插入圖片描述可以看出,兩個執行緒還是依次執行的。

synchronized 修飾程式碼塊

除了直接修飾方法之外,synchronized 還可以作用於程式碼塊,如下程式碼所示:
在這裡插入圖片描述synchronized 作用於程式碼塊時,鎖物件就是跟在後面括號中的物件。上圖中可以看出任何 Object 物件都可以當作鎖物件。

實現細節

synchronized 既可以作用於方法,也可以作用於某一程式碼塊。但在實現上是有區別的。 比如如下程式碼,使用 synchronized 作用於程式碼塊:

在這裡插入圖片描述使用 javap 檢視上述 test1 方法的位元組碼,可以看出,編譯而成的位元組碼中會包含 monitorenter 和 monitorexit 這兩個位元組碼指令。如下所示:
在這裡插入圖片描述你可能已經發現了,上面位元組碼中有 1 個 monitorenter 和 2 個 monitorexit。這是因為虛擬機器需要保證當異常發生時也能釋放鎖。因此 2 個 monitorexit 一個是程式碼正常執行結束後釋放鎖,一個是在程式碼執行異常時釋放鎖。

再看下 synchronized 修飾方法有哪些區別:
在這裡插入圖片描述從圖中可以看出,被 synchronized 修飾的方法在被編譯為位元組碼後,在方法的 flags 屬性中會被標記為 ACC_SYNCHRONIZED 標誌。當虛擬機器訪問一個被標記為 ACC_SYNCHRONIZED 的方法時,會自動在方法的開始和結束(或異常)位置新增 monitorenter 和 monitorexit 指令。

關於 monitorenter 和 monitorexit,可以理解為一把具體的鎖。在這個鎖中儲存著兩個比較重要的屬性:計數器和指標。

  • 計數器代表當前執行緒一共訪問了幾次這把鎖;
  • 指標指向持有這把鎖的執行緒。

用一張圖表示如下:
在這裡插入圖片描述鎖計數器預設為0,當執行monitorenter指令時,如鎖計數器值為0 說明這把鎖並沒有被其它執行緒持有。那麼這個執行緒會將計數器加1,並將鎖中的指標指向自己。當執行monitorexit指令時,會將計數器減1。

ReentrantLock

ReentrantLock 基本使用

ReentrantLock 的使用同 synchronized 有點不同,它的加鎖和解鎖操作都需要手動完成,如下所示:
在這裡插入圖片描述圖中 lock() 和 unlock() 分別是加鎖和解鎖操作。執行效果如下:
在這裡插入圖片描述可以看出,使用 ReentrantLock 也能實現同 synchronized 相同的效果。

在上面 ReentrantLock 的使用中,我將 unlock 操作放在 finally 程式碼塊中。這是因為 ReentrantLock 與 synchronized 不同,當異常發生時 synchronized 會自動釋放鎖,但是 ReentrantLock 並不會自動釋放鎖。因此好的方式是將 unlock 操作放在 finally 程式碼塊中,保證任何時候鎖都能夠被正常釋放掉。

公平鎖實現

ReentrantLock 有一個帶引數的構造器,如下:
在這裡插入圖片描述預設情況下,synchronized 和 ReentrantLock 都是非公平鎖。但是 ReentrantLock 可以通過傳入 true 來建立一個公平鎖。所謂公平鎖就是通過同步佇列來實現多個執行緒按照申請鎖的順序獲取鎖。

公平鎖使用例項如下:
在這裡插入圖片描述執行效果如下:
在這裡插入圖片描述可以看出,建立的 3 個執行緒依次按照順序去修改 sharedNumber 的值。

讀寫鎖(ReentrantReadWriteLock)
在常見的開發中,我們經常會定義一個執行緒間共享的用作快取的資料結構,比如一個較大的 Map。快取中儲存了全部的城市 Id 和城市 name 對應關係。這個大 Map 絕大部分時間提供讀服務(根據城市 Id 查詢城市名稱等)。而寫操作佔有的時間很少,通常是在服務啟動時初始化,然後可以每隔一定時間再重新整理快取的資料。但是寫操作開始到結束之間,不能再有其他讀操作進來,並且寫操作完成之後的更新資料需要對後續的讀操作可見。

在沒有讀寫鎖支援的時候,如果想要完成上述工作就需要使用 Java 的等待通知機制,就是當寫操作開始時,所有晚於寫操作的讀操作均會進入等待狀態,只有寫操作完成並進行通知之後,所有等待的讀操作才能繼續執行。這樣做的目的是使讀操作能讀取到正確的資料,不會出現髒讀。

但是如果使用 concurrent 包中的讀寫鎖(ReentrantReadWriteLock)實現上述功能,就只需要在讀操作時獲取讀鎖,寫操作時獲取寫鎖即可。當寫鎖被獲取到時,後續的讀寫鎖都會被阻塞,寫鎖釋放之後,所有操作繼續執行,這種程式設計方式相對於使用等待通知機制的實現方式而言,變得簡單明瞭。

接下來,我們看下讀寫鎖(ReentrantReadWriteLock)如何使用:

  1. 建立讀寫鎖物件:

在這裡插入圖片描述2. 通過讀寫鎖物件分別獲取讀鎖(ReadLock)和寫鎖(WriteLock):
在這裡插入圖片描述3. 使用讀鎖(ReadLock)同步快取的讀操作,使用寫鎖(WriteLock)同步快取的寫操作:

在這裡插入圖片描述具體實現,參考如下程式碼片段:
在這裡插入圖片描述
解釋說明:

  • 圖中的 number 是執行緒中共享的資料,用來模擬快取資料;
  • 圖中①處,分別建立 2 個 Reader 執行緒並從快取中讀取資料,和 1 個 Writer 將資料寫入快取中;
  • 圖中②處,使用讀鎖(ReadLock)將讀取資料的操作加鎖;
  • 圖中③處,使用寫鎖(WriteLock)將寫入資料到快取中的操作加鎖。

上述程式碼執行效果如下:

在這裡插入圖片描述