Lock介面
鎖是用來控制多個線程訪問共用資源的方式,一般來說,一個鎖能夠防止多個線程同時訪問共用資源(但是有些鎖可以允許多個線程並發的訪問共用資源,比如讀寫鎖)。在Lock介面出現之前,Java程式是靠synchronized關鍵字實現鎖功能的,而Java SE 5之後,並發包中新增了Lock介面(以及相關實作類別)用來實現鎖功能,它提供了與synchronized關鍵字類似的同步功能,只是在使用時需要顯式地擷取和釋放鎖。雖然它缺少了(通過synchronized塊或者方法所提供的)隱式擷取釋放鎖的便捷性,但是卻擁有了鎖擷取與釋放的可操作性、可中斷的擷取鎖以及逾時擷取鎖等多種synchronized關鍵字所不具備的同步特性。
使用synchronized關鍵字將會隱式地擷取鎖,但是它將鎖的擷取和釋放固化了,也就是先擷取再釋放。當然,這種方式簡化了同步的管理,可是擴充性沒有顯示的鎖擷取和釋放來的好。例如,針對一個情境,手把手進行鎖擷取和釋放,先獲得鎖A,然後再擷取鎖B,當鎖B獲得後,釋放鎖A同時擷取鎖C,當鎖C獲得後,再釋放B同時擷取鎖D,以此類推。這種情境下,synchronized關鍵字就不那麼容易實現了,而使用Lock卻容易許多。
Lock的代碼使用樣本:
public class LockTest { public void lockTest(){ Lock lock = new ReentrantLock(); lock.lock(); try{} catch(Exception t){ }finally{ lock.unlock(); } }}
在finally塊中釋放鎖,目的是保證在擷取到鎖之後,最終能夠被釋放。不要將擷取鎖的過程寫在try塊中,因為如果在擷取鎖(自訂鎖的實現)時發生了異常,異常拋出的同時,也會導致鎖無故釋放。 Lock介面方法
void lock();
擷取鎖,調用該方法線程將會擷取鎖,當擷取鎖後從該方法返回
void lockInterruptibly() throws InterruptedException;
可中斷擷取鎖,和lock()方法的不同之處在於該方法會響應中斷,即在鎖的擷取中可以中斷當前線程
boolean tryLock();
嘗試非阻塞的擷取鎖,調用該方法後會立刻返回,擷取成功返回true,否則返回false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
逾時擷取鎖,當前線程在以下情況會返回
1、當前線程在逾時範圍內擷取鎖
2、當前線程在逾時範圍內被中斷
3、逾時時間結束返回false
void unlock();
釋放鎖
Condition newCondition();
擷取等待通知群組件,該組件和當前的鎖綁定,當前線程只有獲得了鎖,才能調用該組件的wait()方法,而調用後,當前線程將釋放鎖。 Lock介面與synchronized的區別
嘗試非阻塞地擷取鎖:當前線程嘗試擷取鎖,如果這一時刻鎖沒有被其他線程擷取到,則成功擷取並持有鎖
能被中斷地擷取鎖:與synchronized不同,擷取到的鎖能夠響應中斷,當擷取到鎖的線程被中斷時,中斷異常將會被拋出,同時鎖會被釋放
逾時擷取鎖:在指定的時間截止之前擷取鎖,如果截止時間之前仍舊無法擷取鎖,則返回 隊列同步器
隊列同步器AbstractQueuedSynchronizer(以下簡稱同步器),是用來構建鎖或者其他同步群組件的基礎架構,它使用了一個int成員變數表示同步狀態,通過內建的FIFO隊列來完成資源擷取線程的排隊工作,並發包的作者(Doug Lea)期望它能夠成為實現大部分同步需求的基礎。同步器的主要使用方式是繼承,子類通過繼承同步器並實現它的抽象方法來管理同步狀態,在抽象方法的實現過程中免不了要對同步狀態變更,這時就需要使用同步器提供的3個方法(getState()、setState(int newState)和compareAndSetState(int expect,int update))來進行操作,因為它們能夠保證狀態的改變是安全的。子類推薦被定義為自訂同步群組件的靜態內部類,同步器自身沒有實現任何同步介面,它僅僅是定義了若干同步狀態擷取和釋放的方法來供自訂同步群組件使用,同步器既可以支援獨佔式地擷取同步狀態,也可以支援共用式地擷取同步狀態,這樣就可以方便實現不同類型的同步群組件(ReentrantLock、ReentrantReadWriteLock和CountDownLatch等)。同步器是實現鎖(也可以是任意同步群組件)的關鍵,在鎖的實現中彙總同步器,利用同步器實現鎖的語義。可以這樣理解二者之間的關係:鎖是面向使用者的,它定義了使用者與鎖互動的介面(比如可以允許兩個線程並行訪問),隱藏了實現細節;同步器面向的是鎖的實現者,它簡化了鎖的實現方式,屏蔽了同步狀態管理、線程的排隊、等待與喚醒等底層操作。鎖和同步器很好地隔離了使用者和實現者所需關注的領域。 隊列同步器介面說明
同步器的設計是基於模板方法模式的,也就是說,使用者需要繼承同步器並重寫指定的方法,隨後將同步器組合在自訂同步群組件的實現中,並調用同步器提供的模板方法,而這些模板方法將會調用使用者重寫的方法。重寫同步器指定的方法時,需要使用同步器提供的如下3個方法來訪問或修改同步狀態。
getState():擷取當前同步狀態
setState():設定當前同步狀態
compareAndSetState(int expect,int update):使用CAS設定目前狀態,該方法能夠保證狀態設定的原子性
同步器可重寫的方法
// 獨佔式擷取同步狀態,實現該方法需要查詢目前狀態並判斷同步狀態是否符合預期,然後通過CAS設定同步狀態protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); }// 獨佔式釋放同步鎖,等待擷取同步狀態的線程有機會擷取到同步狀態protected boolean tryRelease(int arg) { throw new UnsupportedOperationException(); }// 共用式擷取同步狀態,返回大於0表示成功,反之失敗 protected int tryAcquireShared(int arg) { throw new UnsupportedOperationException(); }// 共用式釋放同步狀態protected boolean tryReleaseShared(int arg) { throw new UnsupportedOperationException(); }// 當前同步器是否在獨佔模式下被線程佔用,一般該方法表示是否被當前線程獨佔protected boolean isHeldExclusively() { throw new UnsupportedOperationException(); }
同步器提供的模板方法
獨佔鎖就是在同一時刻只能有一個線程擷取到鎖,而其他擷取鎖的線程只能處於同步隊列中等待,只有擷取鎖的線程釋放了鎖,後繼的線程才能夠擷取鎖
// 獨佔式擷取同步狀態,如果當前線程擷取同步狀態成功,則由該方法返回,否則進入同步隊列等待,該方法將會調用重寫的// tryAccquire方法public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }// 與acquire方法相同,但是該方法響應中斷,當前線程未擷取到同步狀態而進入同步隊列中,如果當前線程被中斷,該方法// 會拋出InterruptedException並返回public final void acquireInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); if (!tryAcquire(arg)) doAcquireInterruptibly(arg); }// 在acquireInterruptibly方法上添加了逾時延遲,如果當前線程在逾時時間內沒有擷取到同步狀態返回false,否則返回truepublic final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout); }// 共用式擷取同步狀態,如果當前線程未擷取到同步狀態,將會進入同步隊列等待,與獨佔式擷取的唯一區別是同一時刻可以有// 多個線程擷取到同步狀態public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) doAcquireShared(arg); }// 與acquireShared,只是該方法響應中斷public final void acquireSharedInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); if (tryAcquireShared(arg) < 0) doAcquireSharedInterruptibly(arg); }// 在acquireShared基礎上添加了逾時間public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); return tryAcquireShared(arg) >= 0 || doAcquireSharedNanos(arg, nanosTimeout); }// 獨佔式釋放同步狀態,該方法會在釋放同步狀態之後將同步隊列第一個節點包含的線程喚醒public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }// 共用式釋放同步狀態 public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; }// 擷取等待在同步隊列上的線程集合 public final Collection<Thread> getQueuedThreads() { ArrayList<Thread> list = new ArrayList<Thread>(); for (Node p = tail; p != null; p = p.prev) { Thread t = p.thread; if (t != null) list.add(t); } return list; }
下面看個獨佔鎖的例子
package com.thread;import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.AbstractQueuedSynchronizer;import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.Lock;public class Mutex implements Lock { // 靜態內部類,自訂同步器 @SuppressWarnings("serial") private static class Sync extends AbstractQueuedSynchronizer{ // 是否處於佔用狀態 protected boolean isHeldExclusively(){ return getState() == 1; } // 當狀態==0擷取鎖,更新state=1 public boolean tryAcquire(int acquires){ if(compareAndSetState(0, 1)){ setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; } // 釋放鎖,將狀態設定為0 public boolean tryRelease(int release){ if(getState() == 0) throw new IllegalMonitorStateException(); setExclusiveOwnerThread(null); setState(0); return true; } // 返回一個condition,每個condition都包含一個condition隊列 Condition newCondition(){ return new ConditionObject(); }; } // 僅需要將操作代理到Sync上 Sync sync = new Sync(); @Override public void lock() { sync.acquire(1); } @Override public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); } @Override public boolean tryLock() { return sync.tryAcquire(1); } @Override public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { return sync.tryAcquireNanos(1, unit.toNanos(time)); } @Override public void unlock() { sync.release(1); } @Override public Condition newCondition() { return sync.newCondition(); } public boolean isLocked() { return sync.isHeldExclusively(); } public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); }}
上述樣本中,獨佔鎖Mutex是一個自訂同步群組件,它在同一時刻只允許一個線程佔有鎖。Mutex中定義了一個靜態內部類,該內部類繼承了同步器並實現了獨佔式擷取和釋放同步狀態。在tryAcquire(int acquires)方法中,如果經過CAS設定成功(同步狀態設定為1),則代表擷取了同步狀態,而在tryRelease(int releases方法中只是將同步狀態重設為0。使用者使用Mutex時並不會直接和內部同步器的實現打交道,而是調用Mutex提供的方法,在Mutex的實現中,以擷取鎖的lock()方法為例,只需要在方法實現中調用同步器的模板方法acquire(int args即可,當前線程調用該方法擷取同步狀態失敗後會被加入到同步隊列中等待,這樣就大大降低了實現一個可靠自訂同步群組件的門檻。 隊列同步器原理分析 隊列同步
同步器依賴內部的同步隊列(一個FIFO雙向隊列)來完成同步狀態的管理,當前線程擷取同步狀態失敗時,同步器會將當前線程以及等待狀態等資訊構造成為一個節點(Node)並將其加入同步隊列,同時會阻塞當前線程,當同步狀態釋放時,會把首節點中的線程喚醒,使其再次嘗試擷取同步狀態。
同步隊列中的節點(Node)用來儲存擷取同步狀態失敗的線程引用、等待狀態以及前驅和後繼節點
/** * 等待狀態 * 1、CANCELLED,值為1,由於同步隊列中等待的線程等待逾時或者被中斷需要從同步隊列中取消等待,節點進入該狀態將不會變化 * 2、SIGNL,值為-1,後繼節點的線程處於等待狀態,而當前節點的線程如果釋放了同步狀態或者被取消,將會通知後繼節點,使後繼節點的線程運行 * 3、CONDITION,值為-2,節點在等待隊列中,節點線程等待在condition上,當其他線程對condition調用了signal()方法後,該節點將會從等待 * 隊列中轉移到同步隊列,加入到同步狀態擷取中 * 4、PROPAGATE,值為-3,表示下一次共用式同步狀態擷取將會被無條件傳播下去 * 5、INITIAL,值為0,初始狀態 */ volatile int waitStatus; /** * 前驅節點,當節點加入同步隊列時被設定,(尾部添加) */ volatile Node prev; /** * 後繼節點 */ volatile Node next; /** * 擷取同步狀態的線程 */ volatile Thread thread; /** * 等待隊列中的後繼節點,如果當前節點是共用的,那麼這個欄位將是shared常量,也就是說節點類型(獨佔、共用)和等待隊列中的後繼節點公用一個欄位 */ Node nextWaiter;
節點是構成同步隊列的基礎,同步器擁有首節點(head)和尾節點(tail),沒有成功擷取同步狀態的線程將會成為節點加入該隊列的尾部,同步隊列的基本結構如下圖所示。
上圖中同步器包含了兩個節點類型的引用,一個指向前端節點,而另一個指向尾節點。試想一下,當一個線程成功地擷取了同步狀態(或者鎖),其他線程將無法擷取到同步狀態,轉而被構造成為節點並加入到同步隊列中,而這個排入佇列的過程必須要保證安全執行緒,因此同步器提供了一個基於CAS的設定尾節點的方法:compareAndSetTail(Node expect,Nodeupdate),它需要傳遞當前線程“認為”的尾節點和當前節點,只有設定成功後,當前節點才正式與之前的尾節點建立關聯。
同步器將節點加入到同步隊列的過程如下圖所示。
同步隊列遵循FIFO,首節點是擷取同步狀態成功的節點,首節點的線程在釋放同步狀態時,將會喚醒後繼節點,而後繼節點將會在擷取同步狀態成功時將自己設定為首節點,該過程如下圖所示。
設定首節點是通過擷取同步狀態成功的線程來完成的,由於只有一個線程能夠成功擷取到同步狀態,因此設定前端節點的方法並不需要使用CAS來保證,它只需要將首節點設定成為原首節點的後繼節點並斷開原首節點的next引用即可。