您好,登錄后才能下訂單哦!
本篇內容介紹了“ReentrantLock是什么意思”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
在Java 5.0之前,在協調對共享對象的訪問的時可以使用的機制只有synchronized 和 volatile。Java 5.0 增加了一種新的機制:ReentrantLock 。與之前提到過的機制相反,ReentrantLock 并不是一種替代內置加鎖的方法,而是當內置解鎖機制不適用時,作為一種可選擇的高級功能。
ReentrantLock 重入鎖實現了 Lock和 java.io.Serializable接口,并提供了與 synchronized相同的互斥性和內存可見性,ReentrantLock 提供了可重入的加鎖語義,能夠對共享資源能夠重復加鎖,即當前線程獲取該鎖再次獲取不會被阻塞,并且與synchronized相比,它還為處理鎖的不可用性提供了更高的靈活性,與此同時,ReentrantLock 還支持公平鎖和非公平鎖兩種方式。
ReentrantLock類層次結構:
ReentrantLock實現了 Lock和 Serializable接口,內部有三個內部類, Sync、 NonfairSync、FairSync Sync是一個抽象類型,它繼承AbstractQueuedSynchronizer,這個AbstractQueuedSynchronizer 是一個模板類,它實現了許多和鎖相關的功能,并提供了鉤子方法供用戶實現,比如tryAcquire、tryRele ase等Sync實現了AbstractQueuedSynchronizer的 tryRelease方法。 NonfairSync和FairSync兩個類繼承自Sync,實現了lock方法,然后分別公平搶占 和非公平搶占針對tryAcquire有不同的實現。
可重入鎖,也叫做 遞歸鎖,從名字上理解,字面意思就是再進入的鎖,重入性是指任意線程在獲取到鎖之后能夠再次獲取該鎖而不會被鎖阻塞,首先他需要具備兩個條件:1) 線程再次獲取鎖:所需要去識別獲取鎖的線程是否為當前占據鎖的線程,如果是,則再次獲取成功 2) 鎖的最終釋放:線程重復n次獲取了鎖,隨后在第n次釋放該鎖后,其它線程能夠獲取到該鎖。鎖的最終釋放要求鎖對于獲取進行計數自增,計數表示當前線程被重復獲取的次數,而被釋放時,計數自減,當計數為0時表示鎖已經成功釋放。
使用ReentrantLock 案例:
Lock lock = new ReentrantLock(); lock.lock(); try{ //更新對象狀態 //捕獲異常,并在必須時恢復不變性條件 }catch (Exception e){ e.printStackTrace(); } finally { lock.unlock(); }
上述代碼中是使用Lock接口的標準使用方式,這種形式比使用內置鎖(synchronized )復雜一些,必須要在 finally 塊中釋放鎖,否則,如果在被保護的代碼中拋出了異常,那么這個鎖永遠都無法釋放。
在簡介中我們知道 ReentrantLock繼承自 Lock接口, Lock提供了一些獲取鎖和釋放鎖的方法,以及條件判斷的獲取的方法,通過實現它來進行鎖的控制,因為它是顯示鎖,所以需要顯示指定起始位置和終止位置,下面就來介紹一下 Lock接口的方法介紹:
ReentrantLock 也實現了上面接口的內容,同時 ReentrantLock 提供了 公平鎖和 非公平鎖兩種模式,如果沒有特別的去指定使用何種方式,那么 ReentrantLock 會默認為 非公平鎖,首先我們來看一下 ReentrantLock 的構造函數:
/** * 無參的構造函數 */ public ReentrantLock() { sync = new NonfairSync(); } /** * 有參構造函數 * 參數為布爾類型 */ public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
從上述源碼中我們可以看到:
ReentrantLock 優先使用的是無參構造函數,也就是非公平鎖,但是當我們調用有參構造函數時,可以指定使用哪種鎖來進行操作(公平鎖還是非公平鎖),參數為布爾類型,如果指定為 false 的話代表 非公平鎖 ,如果指定為 true 的話代表的是 公平鎖
Sync 類 是 ReentrantLock 自定義的同步組件,它是 ReentrantLock 里面的一個內部類,它繼承自AQS(AbstractQueuedSynchronizer),Sync 有兩個子類:公平鎖 FairSync 和 非公平鎖 NonfairSync
ReentrantLock 的獲取與釋放鎖操作都是委托給該同步組件來實現的。下面我們來看一看非公平鎖的 lock() 方法:
1、NonfairSync.lock() 方法流程圖:
2、lock方法詳解
在初始化 ReentrantLock 的時候,如果我們不傳參,使用默認的構造函數,那么默認使用非公平鎖,也就是 NonfairSync2) 當我們調用 ReentrantLock 的 lock() 方法的時候,實際上是調用了 NonfairSync 的 lock() 方法,代碼如下:
static final class NonfairSync extends Sync { private static final long serialVersionUID = 7316153563782823691L; /** * Performs lock. Try immediate barge, backing up to normal * acquire on failure. */ final void lock() { //這個方法先用CAS操作,去嘗試搶占該鎖 // 快速嘗試將state從0設置成1,如果state=0代表當前沒有任何一個線程獲得了鎖 if (compareAndSetState(0, 1)) //state設置成1代表獲得鎖成功 //如果成功,就把當前線程設置在這個鎖上,表示搶占成功,在重入鎖的時候需要 setExclusiveOwnerThread(Thread.currentThread()); else //如果失敗,則調用 AbstractQueuedSynchronizer.acquire() 模板方法,等待搶占。 acquire(1); } protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } }
調用 acquire(1) 實際上使用的是 AbstractQueuedSynchronizer 的acquire() 方法,它是一套鎖搶占的模板,acquire() 代碼比較簡單:
public final void acquire(int arg) { //先去嘗試獲取鎖,如果沒有獲取成功,就在CLH隊列中增加一個當前線程的節點,表示等待搶占。 //然后進入CLH隊列的搶占模式,進入的時候也會去執行一次獲取鎖的操作,如果還是獲取不到, //就調用LockSupport.park() 將當前線程掛起。那么當前線程什么時候會被喚醒呢?當 //持有鎖的那個線程調用 unlock() 的時候,會將CLH隊列的頭節點的下一個節點上的線程 //喚醒,調用的是 LockSupport.unpark() 方法。 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
acquire() 會先調用 tryAcquire() 這個鉤子方法去嘗試獲取鎖,這個方法就是在 NonfairSync.tryAcquire()下的 nonfairTryAcquire(),源碼如下:
//一個嘗試插隊的過程 final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); //獲取state值 int c = getState(); //比較鎖的狀態是否為 0,如果是0,當前沒有任何一個線程獲取鎖 if (c == 0) { //則嘗試去原子搶占這個鎖(設置狀態為1,然后把當前線程設置成獨占線程) if (compareAndSetState(0, acquires)) { // 設置成功標識獨占鎖 setExclusiveOwnerThread(current); return true; } } //如果當前鎖的狀態不是0 state!=0,就去比較當前線程和占用鎖的線程是不是一個線程 else if (current == getExclusiveOwnerThread()) { //如果是,增加狀態變量的值,從這里看出可重入鎖之所以可重入,就是同一個線程可以反復使用它占用的鎖 int nextc = c + acquires; //重入次數太多,大過Integer.MAX if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } //如果以上兩種情況都不通過,則返回失敗false return false; }
tryAcquire() 一旦返回 false,就會則進入 acquireQueued() 流程,也就是基于CLH隊列的搶占模式,在CLH鎖隊列尾部增加一個等待節點,這個節點保存了當前線程,通過調用 addWaiter() 實現,這里需要考慮初始化的情況,在第一個等待節點進入的時候,需要初始化一個頭節點然后把當前節點加入到尾部,后續則直接在尾部加入節點。
代碼如下:
//AbstractQueuedSynchronizer.addWaiter() private Node addWaiter(Node mode) { // 初始化一個節點,用于保存當前線程 Node node = new Node(Thread.currentThread(), mode); // 當CLH隊列不為空的視乎,直接在隊列尾部插入一個節點 Node pred = tail; if (pred != null) { node.prev = pred; //如果pred還是尾部(即沒有被其他線程更新),則將尾部更新為node節點(即當前線程快速設置成了隊尾) if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } // 當CLH隊列為空的時候,調用enq方法初始化隊列 enq(node); return node; } private Node enq(final Node node) { //在一個循環里不停的嘗試將node節點插入到隊尾里 for (;;) { Node t = tail; if (t == null) { // 初始化節點,頭尾都指向一個空節點 if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
將節點增加到CLH隊列后,進入 acquireQueued() 方法
final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; //在一個循環里不斷等待前驅節點執行完畢 for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) {// 通過tryAcquire獲得鎖,如果獲取到鎖,說明頭節點已經釋放了鎖 setHead(node);//將當前節點設置成頭節點 p.next = null; // help GC//將上一個節點的next變量被設置為null,在下次GC的時候會清理掉 failed = false;//將failed標記設置成false return interrupted; } //中斷 if (shouldParkAfterFailedAcquire(p, node) && // 是否需要阻塞 parkAndCheckInterrupt())// 阻塞,返回線程是否被中斷 interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
如果嘗試獲取鎖失敗,就會進入 shouldParkAfterFailedAcquire() 方法,會判斷當前線程是否阻塞
/** * 確保當前結點的前驅結點的狀態為SIGNAL * SIGNAL意味著線程釋放鎖后會喚醒后面阻塞的線程 * 只有確保能夠被喚醒,當前線程才能放心的阻塞。 */ private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) //如果前驅節點狀態為SIGNAL //表明當前線程需要阻塞,因為前置節點承諾執行完之后會通知喚醒當前節點 return true; if (ws > 0) {//ws > 0 代表前驅節點取消了 do { node.prev = pred = pred.prev;//不斷的把前驅取消了的節點移除隊列 } while (pred.waitStatus > 0); pred.next = node; } else { //初始化狀態,將前驅節點的狀態設置成SIGNAL compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }
當進入阻塞階段,會進入parkAndCheckInterrupt() 方法,則會調用 LockSupport.park(this) 將當前線程掛起。代碼如下:
// 從方法名可以看出這個方法做了兩件事 private final boolean parkAndCheckInterrupt() { LockSupport.park(this);//掛起當前的線程 // 如果當前線程已經被中斷了,返回true,否則返回false // 有可能在掛起階段被中斷了 return Thread.interrupted(); }
2.1 unlock()方法的示意圖
2.1 unlock()方法詳解1) 調用 unlock() 方法,其實是直接調用 AbstractQueuedSynchronizer.release() 操作。
2) 進入 release() 方法,內部先嘗試 tryRelease() 操作,主要是去除鎖的獨占線程,然后將狀態減一,這里減一主要是考慮到可重入鎖可能自身會多次占用鎖,只有當狀態變成0,才表示完全釋放了鎖。
3) 如果 tryRelease 成功,則將CHL隊列的頭節點的狀態設置為0,然后喚醒下一個非取消的節點線程。
4) 一旦下一個節點的線程被喚醒,被喚醒的線程就會進入 acquireQueued() 代碼流程中,去獲取鎖。
代碼如下:
public void unlock() { sync.release(1); } public final boolean release(int arg) { //嘗試在當前鎖的鎖定計數(state)值上減1, if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0)//waitStatus!=0表明或者處于CANCEL狀態,或者是SIGNAL表示下一個線程在等待其喚醒。也就是說waitStatus不為零表示它的后繼在等待喚醒。 unparkSuccessor(h); //成功返回true return true; } //否則返回false return false; } private void unparkSuccessor(Node node) { int ws = node.waitStatus; //如果waitStatus < 0 則將當前節點清零 if (ws < 0) compareAndSetWaitStatus(node, ws, 0); //若后續節點為空或已被cancel,則從尾部開始找到隊列中第一個waitStatus<=0,即未被cancel的節點 Node s = node.next; if (s == null || s.waitStatus > 0) { s = null; for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) LockSupport.unpark(s.thread); }
當然在 release() 方法中不僅僅只是將 state - 1 這么簡單,-1 之后還需要進行一番處理,如果 -1 之后的 新state = 0 ,則表示當前鎖已經被線程釋放了,同時會喚醒線程等待隊列中的下一個線程。
protected final boolean tryRelease(int releases) { int c = getState() - releases; //判斷是否為當前線程在調用,不是拋出IllegalMonitorStateException異常 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; //c == 0,釋放該鎖,同時將當前所持有線程設置為null if (c == 0) { free = true; setExclusiveOwnerThread(null); } //設置state setState(c); return free; } private void unparkSuccessor(Node node) { int ws = node.waitStatus; if (ws < 0) compareAndSetWaitStatus(node, ws, 0); Node s = node.next; if (s == null || s.waitStatus > 0) { s = null; // 從后往前找到離head最近,而且waitStatus <= 0 的節點 // 其實在ReentrantLock中,waitStatus應該只能為0和-1,需要喚醒的都是-1(Node.SIGNAL) for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) LockSupport.unpark(s.thread);// 喚醒掛起線程 }
重點:unlock最好放在finally中,因為如果沒有使用finally來釋放Lock,那么相當于啟動了一個定時炸彈,如果發生錯誤,我們很難追蹤到最初發生錯誤的位置,因為沒有記錄應該釋放鎖的位置和時間,這也就是 ReentrantLock 不能完全替代 synchronized 的原因,因為當程序執行控制離開被保護的代碼塊時,不會自動清除鎖。
FairSync相對來說就簡單很多,只有重寫的兩個方法跟NonfairSync不同
final void lock() { acquire(1); } 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; }
鎖的公平性是相對于獲取鎖的順序而言的。
如果是一個公平鎖,那么鎖的獲取順序就應該符合請求的絕對時間順序,也就是FIFO,線程獲取鎖的順序和調用lock的順序一樣,能夠保證老的線程排隊使用鎖,新線程仍然排隊使用鎖。
非公平鎖只要CAS設置同步狀態成功,則表示當前線程獲取了鎖,線程獲取鎖的順序和調用lock的順序無關,全憑運氣,也就是老的線程排隊使用鎖,但是無法保證新線程搶占已經在排隊的線程的鎖。
ReentrantLock默認使用非公平鎖是基于性能考慮,公平鎖為了保證線程規規矩矩地排隊,需要增加阻塞和喚醒的時間開銷。如果直接插隊獲取非公平鎖,跳過了對隊列的處理,速度會更快。
ReenTrantLock(可重入鎖) 的字面意思就是再進入的鎖,對于 synchronized 關鍵字所使用的鎖也是可重入的,兩者關于這個的區別不大。兩者都是同一個線程沒進入一次,鎖的計數器都自增1,所以要等到鎖的計數器下降為0時才能釋放鎖。
Synchronized 是依賴于JVM實現的,而 ReenTrantLock 是JDK實現的,有什么區別,說白了就類似于操作系統來控制實現和用戶自己敲代碼實現的區別。前者的實現是比較難見到的,后者有直接的源碼可供閱讀。
在Synchronized優化以前,synchronized的性能是比ReenTrantLock差很多的,但是自從Synchronized引入了偏向鎖,輕量級鎖(自旋鎖)后,兩者的性能就差不多了,在兩種方法都可用的情況下,官方甚至建議使用synchronized,其實synchronized的優化我感覺就借鑒了ReenTrantLock中的CAS技術。都是試圖在用戶態就把加鎖問題解決,避免進入內核態的線程阻塞。
便利性:很明顯Synchronized的使用比較方便簡潔,并且由編譯器去保證鎖的加鎖和釋放,而ReenTrantLock需要手工聲明來加鎖和釋放鎖,為了避免忘記手工釋放鎖造成死鎖,所以最好在finally中聲明釋放鎖。
鎖的細粒度和靈活度:很明顯 ReenTrantLock 優于 Synchronized ,但是 ReenTrantLock 沒有辦法完全取代 Synchronized
ReenTrantLock獨有的能力
1) ReenTrantLock 可以指定是公平鎖還是非公平鎖。而 synchronized 只能是非公平鎖。所謂的公平鎖就是先等待的線程先獲得鎖。
2) ReenTrantLock 提供了一個 Condition(條件)類,用來實現分組喚醒需要喚醒的線程們,而不是像synchronized 要么隨機喚醒一個線程要么喚醒全部線程。
3) ReenTrantLock 提供了一種能夠中斷等待鎖的線程的機制,通過 lock.lockInterruptibly() 來實現這個機制。
ReentrantLock 是java中非常重要的一個并發工具,相比于java原生的 synchronized 有著更好的性能,學習 ReentrantLock ,我們主要需要了解它,公平鎖 和 非公平鎖 的實現,以及重入鎖的獲取與釋放的流程,還有最重要的就是要了解AQS(AbstractQueuedSynchronizer),這是實現重入鎖的基礎,ReentrantLock 是一個比較輕量級的鎖,而且使用面向對象的思想去實現了鎖的功能,比原來的synchronized 關鍵字更加好理解。
“ReentrantLock是什么意思”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。