Java併發包獨佔鎖ReentrantLock

前幾篇文章我對同步佇列框架AQS做了詳細的講解,接下來我將介紹在JDK中它使用的地方,AQS包含兩種模式:獨佔資源模式和共享資源模式,本篇文章將例項講解獨佔資源模式的用法,ReentrantLock內部就是實現AQS框架的獨佔模式,它和synchronized一樣是可重入的獨佔鎖。

一、舉例說明ReentrantLock的用法

如果有一個共享變數count,有10個執行緒對它進行累加,每一個執行緒累加1000次,這段程式碼怎樣設計呢?

我們有很多種辦法,可以利用synchronized關鍵字,也可以利用原子類AtomicInteger,那我們利用ReentrantLock怎樣處理的

public class ReentrantLockTest { private static int count = 0; private static ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) throws InterruptedException { for(int i=0;i<10;i++){ new Thread(()->{ try{ lock。lock(); for(int j=0;j<1000;j++){ count++; } }finally { lock。unlock(); } })。start(); } Thread。sleep(5000); System。out。println(“count=”+count); }}

如果上面的程式碼不加鎖的情況下,可能每一次運算的結果都不相同,而加鎖後每一次執行結果都是一樣的。執行結果如圖:

Java併發包獨佔鎖ReentrantLock

上面是鎖最簡單的一個應用,在上一篇文章講解AQS的Condition時,我利用了synchronized+wait()+notify()/notifyAll()闡述了生產者和消費者模式,那與之匹配的另一種方法如下:

上一篇文章的緩衝區程式碼改變如下:

public class SyncCache { //緩衝區 private String[] data = new String[10]; //緩衝區陣列索引 private int index; //建立一個鎖 private ReentrantLock lock = new ReentrantLock(); private Condition condition = lock。newCondition(); //生產資料的方法 public void product(String productData) { try { lock。lock(); if (index == data。length) { System。out。println(“緩衝區已滿,生產者被阻塞”); try { condition。await(); } catch (InterruptedException e) { e。printStackTrace(); } } data[index++] = productData; condition。signal(); } finally { lock。unlock(); } } //消費資料的方法 public String consume() { try { lock。lock(); if (index == 0) { System。out。println(“緩衝區已空,消費者被阻塞”); try { condition。await(); } catch (InterruptedException e) { e。printStackTrace(); } } condition。signal(); index——; return data[index]; } finally { lock。unlock(); } }}

執行結果如下圖:

Java併發包獨佔鎖ReentrantLock

二、從原始碼角度分析ReentrantLock

從上面的demo我們學會了使用ReentrantLock,那它的內部實現原理是怎樣的呢?接下來我們進入它的原始碼一探究竟。

首先透過一張圖來整體瞭解一下ReentrantLock的全貌:

Java併發包獨佔鎖ReentrantLock

從上面的demo中獲取lock()方法,釋放鎖unlock()方法

//獲取獨佔鎖public void lock() { sync。lock();}//釋放獨佔鎖public void unlock() { sync。release(1);}

上面鎖的獲取和釋放的原始碼並沒有更多的邏輯,而核心的邏輯都交給了Sync,那麼Sync又是什麼呢?在講解AQS時我說過,在併發包中鎖的底層實現都是透過AQS框架實現的,

如果想實現獨佔鎖,子類只需要實現如下方法:

1:獲取鎖:tryAcquire()2:釋放鎖:tryRelease()

如果想實現共享鎖:

1:獲取鎖:tryAcquireShared()2:釋放鎖:tryReleaseShared()

Sync就是AQS的子類,並且是獨佔鎖模式。如圖

Java併發包獨佔鎖ReentrantLock

在ReentrantLock中有兩種模式:一種是非公平模式獲取鎖,另一種是公平模式獲取鎖。預設情況下是非公平的,我們看一下構造方法

//預設情況下是非公平模式public ReentrantLock() { sync = new NonfairSync();}//如果fair=true:表示是公平模式獲取鎖//如果fair=false:表示是非公平模式獲取鎖public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync();}

1:非公平模式獲取鎖:也是預設的模式

static final class NonfairSync extends Sync { //非公平下獲取鎖 final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread。currentThread()); else acquire(1); }//嘗試獲取資源 protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); }}

從上面程式碼中可以看出,在非公平模式下,執行緒首先透過CAS機制嘗試獲取鎖,如果獲成功,則代表獲取鎖成功,如果獲取失敗,則直接呼叫AQS中的acquire()方法,而在講解AQS之獨佔模式下資源的獲取已經講解過,acquire()方法首先會呼叫tryAcquire()方法嘗試獲取資源,如果獲取失敗則加入到CLH等待佇列中,而非公平的tryAcquire()直接呼叫了父類Sync中的nonfairAcquire()方法。

final boolean nonfairTryAcquire(int acquires) { //獲取當前執行緒 final Thread current = Thread。currentThread();//獲取共享資源state int c = getState();//如果共享資源等於0:說明還沒有執行緒獲取到資源 if (c == 0) {//透過CAS機制將資源從0變成acquires,成功則說明獲取到鎖 if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } }//如果共享資源不等於0,但是獲取資源的是當前執行緒,由於是可重入鎖 else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error(“Maximum lock count exceeded”); setState(nextc); return true; } return false;}

這個方法非常的簡單,首先透過CAS機制獲取資源,如果成功則說明獲取鎖成功。如果不成功,則需要判斷擁有資源的執行緒是否是當前執行緒,如果是當前執行緒,由於鎖是可重入的,所以成功獲取到鎖,如果擁有資源的執行緒不是當前執行緒,則獲取鎖失敗。

非公平模式下tryAcquire()獲取共享資源的流程如下:

Java併發包獨佔鎖ReentrantLock

2:公平模式獲取鎖

final void lock() { acquire(1);}

考慮到公平原則,並沒有像非公平模式下那樣上來第一步就嘗試獲取鎖,這樣做的原因:可能在CLH等待佇列中存在等待獲取鎖的執行緒,按照公平性,需要直接呼叫AQS中的acquire()方法,讓它去判斷是獲取鎖還是放到等待佇列中,在公平模式下的tryAcquire()如下:

protected final boolean tryAcquire(int acquires) { final Thread current = Thread。currentThread(); int c = getState(); if (c == 0) { if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error(“Maximum lock count exceeded”); setState(nextc); return true; } return false;}

上面的程式碼和在非公平模式下的程式碼極其類似,但是有一個非常重要的不同點就是:

在公平模式下當共享資源還沒有被任何執行緒獲取時,它並沒有直接透過CAS機制獲取資源,而是首先呼叫hasQueuedPredecessors()方法來判斷等待佇列中是否有比自己早的執行緒在等待獲取資源

,流程圖如下:

Java併發包獨佔鎖ReentrantLock

上面我分析了ReentrantLock公平模式和非公平模式獲取鎖的不同,一句話總結:

非公平性允許執行緒插隊獲取資源,而公平性模式則不允許插隊。

上面講解了獲取鎖的過程,接下來就是釋放鎖了,釋放鎖就是釋放獲取到的資源,公平模式和非公平模式在釋放鎖時機制是一樣的,所以釋放鎖在他們的父類中。

protected final boolean tryRelease(int releases) { int c = getState() - releases; if (Thread。currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free;}

首先判斷當前執行緒是否是獲取鎖的執行緒,如果不是則直接丟擲異常,然後判斷此時的狀態是否為0,因為是可重入鎖,所以把全部的資源都釋放才能釋放鎖,所以當共享資源為0時說明資源已全部釋放。

Java併發包獨佔鎖ReentrantLock