根據不同的業務場景,選擇合適的鎖?
前言:剛開始我看到這個標題的時候我感覺“很熟悉,但是又很陌生”,因為鎖是有效的解決并發情況下保證臨界資源操作原子性的有效手段之一。下面我就從我們幾個開發使用的角度來說我們常用的鎖。
鎖可以解決什么問題?
鎖可以解決并行執行任務執行過程中對,共享數據順序訪問、修改的場景。比如對同一個賬戶進行并行扣款或者轉賬。下面我們展開討論下 synchronized 、ReetranLock 以及他們的使用。
synchronized
synchronized 是 JDK 提供的內置鎖, 由 JVM 虛擬機內部實現,是基于 monitor 機制, 在 JDK 1.6 之后被優化,會有一個鎖升級的過程,將鎖的狀態存儲到對象頭中。
鎖升級過程,默認是無鎖狀態,首先會進行判斷,如果是沒有字段競爭的情況下會使用偏向鎖,偏向鎖的本質就是將當前獲得鎖的線程 id 設置到共享數據的對象頭中。然后升級為輕量級鎖,輕量級鎖的本質是通過 CAS 來修改 MarkWord 來實現的。最后再升級為重量級鎖,我們可以通過操作系統的 monitor 依賴操作系統的 MutexLock(互斥鎖)來實現的 。
四種使用方式
- 在靜態方法上使用
- 在普通方法上使用
- 鎖定 this 狀態
- 鎖定靜態類
加鎖狀態記錄位置
對象加鎖,記錄在對象頭中,對象頭如下圖所示。
在運行期間,Mark Word里面存儲的數據會隨著鎖標志位的變化而變化。Mark Word可能變為存儲以下4種數據,如下圖所示
鎖的膨脹和升級
鎖的升級和膨脹時候不可逆轉的。
使用場景
JDK 在并發包中, 使用 synchroinzed 的地方有:
- ConcurrentHashMap (jdk 1.8)
- HashTable
ReetrantLock
ReetrantLock 開發作者是 Doug Lea ,從 JDK1.5 開始過后加入 JDK 的鎖,主要是通過 QAS 的方式來實現的, 通過 Unsafe 包提供的 CAS 操作來進行鎖狀態(state)的競爭。然后通過 LockSupport.park(this). 進行 park 住線程,如果在 AQS 隊列頭的對象進行喚醒執行 unpack 方法,然后讓他去競爭鎖。
ReetrantLock 還分為公平鎖和非公平鎖,默認是非公平鎖。因為公平鎖,是需要保證競爭者按照獲取鎖的順序進行獲得,性能略低于非公平鎖。
AQS 隊列結構如下所示,它的本質是一個 FIFO 的線程安全的同步隊列,如下圖所示:
ReetrantLock 加鎖和解鎖的過程如下圖所示:
使用方式
ReetrantLock 的使用方式如下,主要是有三個步驟:創建、加鎖、解鎖。
- class X {
- private final ReentrantLock lock = new ReentrantLock();
- // ...
- public void m() {
- lock.lock(); // block until condition holds
- try {
- // ... method body
- } finally {
- lock.unlock()
- }
- }
- }
使用場景
JDK 在并發包中, 使用 ReetrantLock 的地方有:
- CyclicBarrier
- DelayQueue
- LinkedBlockingDeque
- ThreadPoolExecutor
- ReentrantReadWriteLock
- StampedLock
上面我只是列舉了一部分,對于 ReetrantLock 來看可以說是并發包中非常基礎的類,也是我們學習并發的基礎,在后續的文章中我會給展開做更加深入的分析。
如何選擇鎖?
1.對于單機環境我們在 JDK 內進行并發控制我們可以使用 synchronized (內置鎖) 和 RentrantLock 。
2.對于自增或者原子數據累計我們可以使用 Unsafe 提供的原子類,比如 AtomicInteger , AtomicLong
3.對于數據庫的話,對于用戶金額扣除的場景我們可以使用樂觀鎖的方式來進行控制,SQL 如下
- update table_name set amount = 100,
- version = version + 1 where id = 1 and version = 1;
4.對于分布式場景下我們需要保證一致性,可以使用 Redis 或者 Zk 實現分布式鎖。來進行分布式場景下的并發控制。
參考信息
《深入理解 Java 虛擬機》周志明
https://blog.csdn.net/wangbo199308/article/details/108688109
【編輯推薦】