1. 程式人生 > >Java中的鎖

Java中的鎖

5.1 Lock介面

       鎖是用來控制多個執行緒訪問共享資源的方式,一般來說,一個鎖能夠防止多個執行緒同時訪問共享資源(但是有些鎖可以允許多個執行緒併發的訪問共享資源,比如讀寫鎖)。在Lock接口出現之前,Java程式是靠synchronized關鍵字實現鎖功能的,而Java SE 5之後,併發包中新增了Lock介面(以及相關實現類)用來實現鎖功能,它提供了與synchronized關鍵字類似的同步功能,只是在使用時需要顯式地獲取和釋放鎖。雖然它缺少了(通過synchronized塊或者方法所提供的)隱式獲取釋放鎖的便捷性,但是卻擁有了鎖獲取與釋放的可操作性、可中斷的獲取鎖以及超時獲取鎖等多種synchronized關鍵字所不具備的同步特性。        使用synchronized關鍵字將會隱式地獲取鎖,但是它將鎖的獲取和釋放固化了,也就是先獲取再釋放。當然,這種方式簡化了同步的管理,可是擴充套件性沒有顯示的鎖獲取和釋放來的好。例如,針對一個場景,手把手進行鎖獲取和釋放,先獲得鎖A,然後再獲取鎖B,當鎖B獲得後,釋放鎖A同時獲取鎖C,當鎖C獲得後,再釋放B同時獲取鎖D,以此類推。這種場景下,synchronized關鍵字就不那麼容易實現了,而使用Lock卻容易許多。Lock的使用也很簡單,程式碼清單5-1是Lock的使用的方式:

LockUseCase.java
Lock lock = new ReentrantLock();
lock.lock();
try {
} finally {
	lock.unlock();
}

在finally塊中釋放鎖,目的是保證在獲取到鎖之後,最終能夠被釋放。不要將獲取鎖的過程寫在try塊中,因為如果在獲取鎖(自定義鎖的實現)時發生了異常,異常丟擲的同時,也會導致鎖無故釋放。

Lock介面提供的synchronized關鍵字所不具備的主要特性如下所示:

Lock是一個介面,它定義了鎖獲取和釋放的基本操作,Lock的API如下所示:

5.2 佇列同步器

       佇列同步器AbstractQueuedSynchronizer(以下簡稱同步器),是用來構建鎖或者其他同步元件的基礎框架,它使用了一個int成員變量表示同步狀態,通過內建的FIFO佇列來完成資源獲取執行緒的排隊工作,併發包的作者(Doug Lea)期望它能夠成為實現大部分同步需求的基礎。

       同步器的主要使用方式是繼承,子類通過繼承同步器並實現它的抽象方法來管理同步狀態,在抽象方法的實現過程中免不了要對同步狀態進行更改,這時就需要使用同步器提供的3個方法(getState()、setState(int newState)和compareAndSetState(int expect,int update))來進行操作,因為它們能夠保證狀態的改變是安全的。子類推薦被定義為自定義同步元件的靜態內部類,同步器自身沒有實現任何同步介面,它僅僅是定義了若干同步狀態獲取和釋放的方法來供自定義同步元件使用,同步器既可以支援獨佔式地獲取同步狀態,也可以支援共享式地獲取同步狀態,這樣就可以方便實現不同型別的同步元件(ReentrantLock、ReentrantReadWriteLock和CountDownLatch等)。        同步器是實現鎖(也可以是任意同步元件)的關鍵,在鎖的實現中聚合同步器,利用同步器實現鎖的語義。可以這樣理解二者之間的關係:鎖是面向使用者的,它定義了使用者與鎖互動的介面(比如可以允許兩個執行緒並行訪問),隱藏了實現細節;同步器面向的是鎖的實現者,它簡化了鎖的實現方式,遮蔽了同步狀態管理、執行緒的排隊、等待與喚醒等底層操作。鎖和同步器很好地隔離了使用者和實現者所需關注的領域 5.2.1 佇列同步器的介面與示例 同步器的設計是基於模板方法模式的,也就是說,使用者需要繼承同步器並重寫指定的方法,隨後將同步器組合在自定義同步元件的實現中,並呼叫同步器提供的模板方法,而這些模板方法將會呼叫使用者重寫的方法。重寫同步器指定的方法時,需要使用同步器提供的如下3個方法來訪問或修改同步狀態:

  • getState():獲取當前同步狀態
  • setState(int newState):設定當前同步狀態
  • compareAndSetState(int expect,int update):使用CAS設定當前狀態,該方法能夠保證狀態設定的原子性

同步器可重寫的方法與描述如下所示:

實現自定義同步元件時,將會呼叫同步器提供的模板方法,這些(部分)模板方法與描述如下所示:

同步器提供的模板方法基本上分為3類:獨佔式獲取與釋放同步狀態、共享式獲取與釋放同步狀態和查詢同步佇列中的等待執行緒情況。自定義同步元件將使用同步器提供的模板方法來實現自己的同步語義。只有掌握了同步器的工作原理才能更加深入地理解併發包中其他的併發元件,所以下面通過一個獨佔鎖的示例來深入瞭解一下同步器的工作原理,顧名思義,獨佔鎖就是在同一時刻只能有一個執行緒獲取到鎖,而其他獲取鎖的執行緒只能處於同步佇列中等待,只有獲取鎖的執行緒釋放了鎖,後繼的執行緒才能夠獲取鎖,如程式碼所示:

Mutex.java
class Mutex implements Lock {
	// 靜態內部類,自定義同步器
	private static class Sync extends AbstractQueuedSynchronizer {
		// 是否處於佔用狀態
		protected boolean isHeldExclusively() {
			return getState() == 1;
		}
		//當狀態為0的時候獲取鎖
		public boolean tryAcquire(int acquires) {
			if (compareAndSetState(0, 1)) {
			setExclusiveOwnerThread(Thread.currentThread());
			return true;
		}
		return false;
		}
		//釋放鎖,將狀態設定為0
		protected boolean tryRelease(int releases) {
			if (getState() == 0) throw new
			IllegalMonitorStateException();
			setExclusiveOwnerThread(null);
			setState(0);
			return true;
		}
		//返回一個Condition,每個condition都包含了一個condition佇列
		Condition newCondition() { return new ConditionObject(); }
	}
	// 僅需要將操作代理到Sync上即可
	private final Sync sync = new Sync();
	public void lock() { sync.acquire(1); }
	public boolean tryLock() { return sync.tryAcquire(1); }
	public void unlock() { sync.release(1); }
	public Condition newCondition() { return sync.newCondition(); }
	public boolean isLocked() { return sync.isHeldExclusively(); }
	public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); }
	public void lockInterruptibly() throws InterruptedException {
	sync.acquireInterruptibly(1);
	}
	public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
		return sync.tryAcquireNanos(1, unit.toNanos(timeout));
	}
}

上述示例中,獨佔鎖Mutex是一個自定義同步元件,它在同一時刻只允許一個執行緒佔有鎖。Mutex中定義了一個靜態內部類,該內部類繼承了同步器並實現了獨佔式獲取和釋放同步狀態。在tryAcquire(int acquires)方法中,如果經過CAS設定成功(同步狀態設定為1),則代表獲取了同步狀態,而在tryRelease(int releases)方法中只是將同步狀態重置為0。使用者使用Mutex時並不會直接和內部同步器的實現打交道,而是呼叫Mutex提供的方法,在Mutex的實現中,以獲取鎖的lock()方法為例,只需要在方法實現中呼叫同步器的模板方法acquire(int args)即可,當前執行緒呼叫該方法獲取同步狀態失敗後會被加入到同步佇列中等待,這樣就大大降低了實現一個可靠自定義同步元件的門檻。 5.2.2 佇列同步器的實現分析 接下來將從實現角度分析同步器是如何完成執行緒同步的,主要包括:同步佇列、獨佔式同步狀態獲取與釋放、共享式同步狀態獲取與釋放以及超時獲取同步狀態等同步器的核心資料結構與模板方法。 1.同步佇列 同步器依賴內部的同步佇列(一個FIFO雙向佇列)來完成同步狀態的管理,當前執行緒獲取同步狀態失敗時,同步器會將當前執行緒以及等待狀態等資訊構造成為一個節點(Node)並將其加入同步佇列,同時會阻塞當前執行緒,當同步狀態釋放時,會把首節點中的執行緒喚醒,使其再次嘗試獲取同步狀態,同步佇列中的節點(Node)用來儲存獲取同步狀態失敗的執行緒引用、等待狀態以及前驅和後繼節點,節點的屬性型別與名稱以及描述如下所示:

節點是構成同步佇列(等待佇列,在5.6節中將會介紹)的基礎,同步器擁有首節點(head)和尾節點(tail),沒有成功獲取同步狀態的執行緒將會成為節點加入該佇列的尾部,同步佇列的基本結構如圖5-1所示

在圖5-1中,同步器包含了兩個節點型別的引用,一個指向頭節點,而另一個指向尾節點。試想一下,當一個執行緒成功地獲取了同步狀態(或者鎖),其他執行緒將無法獲取到同步狀態,轉而被構造成為節點並加入到同步佇列中,而這個加入佇列的過程必須要保證執行緒安全,因此同步器提供了一個基於CAS的設定尾節點的方法:compareAndSetTail(Node expect,Nodeupdate),它需要傳遞當前執行緒“認為”的尾節點和當前節點,只有設定成功後,當前節點才正式與之前的尾節點建立關聯。 同步器將節點加入到同步佇列的過程如圖5-2所示:

同步佇列遵循FIFO,首節點是獲取同步狀態成功的節點,首節點的執行緒在釋放同步狀態時,將會喚醒後繼節點,而後繼節點將會在獲取同步狀態成功時將自己設定為首節點,該過程如圖5-3所示:

在圖5-3中,設定首節點是通過獲取同步狀態成功的執行緒來完成的,由於只有一個執行緒能夠成功獲取到同步狀態,因此設定頭節點的方法並不需要使用CAS來保證,它只需要將首節點設定成為原首節點的後繼節點並斷開原首節點的next引用即可。 2.獨佔式同步狀態獲取與釋放 通過呼叫同步器的acquire(int arg)方法可以獲取同步狀態,該方法對中斷不敏感,也就是由於執行緒獲取同步狀態失敗後進入同步佇列中,後續對執行緒進行中斷操作時,執行緒不會從同步佇列中移出,該方法程式碼如下所示

同步器的acquire方法
public final void acquire(int arg) {
	if (!tryAcquire(arg) &&
		acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
		selfInterrupt();
}

上述程式碼主要完成了同步狀態獲取、節點構造、加入同步佇列以及在同步佇列中自旋等待的相關工作,其主要邏輯是:首先呼叫自定義同步器實現的tryAcquire(int arg)方法,該方法保證執行緒安全的獲取同步狀態,如果同步狀態獲取失敗,則構造同步節點(獨佔式 Node.EXCLUSIVE,同一時刻只能有一個執行緒成功獲取同步狀態)並通過addWaiter(Node node)方法將該節點加入同步佇列的尾部,最後呼叫acquireQueued(Node node,int arg)方法,使得該節點以“死迴圈”的方式獲取同步狀態。如果獲取不到則阻塞節點中的執行緒,而被阻塞執行緒的喚醒主要依靠前驅節點的出隊或阻塞執行緒被中斷來實現。下面分析一下相關工作。首先是節點的構造以及加入同步佇列,如程式碼:

同步器的addWaiter和enq方法
private Node addWaiter(Node mode) {
	Node node = new Node(Thread.currentThread(), mode);
	// Try the fast path of enq; backup to full enq on failure
	Node pred = tail;
	if (pred != null) {
		node.prev = pred;
		if (compareAndSetTail(pred, node)) {
			pred.next = node;
			return node;
		}
	}
	enq(node);
	return node;
}

private Node enq(final Node node) {
	for (;;) {
		Node t = tail;
		if (t == null) { // Must initialize
			if (compareAndSetHead(new Node()))
				tail = head;
		} else {
			node.prev = t;
			if (compareAndSetTail(t, node)) {
				t.next = node;
				return t;
			}
		}
	}
}

上述程式碼通過使用compareAndSetTail(Node expect,Node update)方法來確保節點能夠被執行緒安全新增。在enq(final Node node)方法中,同步器通過“死迴圈”來保證節點的正確新增,在“死迴圈”中只有通過CAS將節點設定成為尾節點之後,當前執行緒才能從該方法返回,否則,當前執行緒不斷地嘗試設定。可以看出,enq(final Node node)方法將併發新增節點的請求通過CAS變得“序列化”了。節點進入同步佇列之後,就進入了一個自旋的過程,每個節點(或者說每個執行緒)都在自省地觀察,當條件滿足,獲取到了同步狀態,就可以從這個自旋過程中退出,否則依舊留在這個自旋過程中(並會阻塞節點的執行緒),如程式碼所示:

 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)) {
				setHead(node);
				p.next = null; // help GC
				failed = false;
				return interrupted;
			}
			if (shouldParkAfterFailedAcquire(p, node) &&
				parkAndCheckInterrupt())
				interrupted = true;
		}
	} finally {
		if (failed)
			cancelAcquire(node);
	}
}

在acquireQueued(final Node node,int arg)方法中,當前執行緒在“死迴圈”中嘗試獲取同步狀態,而只有前驅節點是頭節點才能夠嘗試獲取同步狀態,這是為什麼? 原因有兩個,如下:

  • 第一,頭節點是成功獲取到同步狀態的節點,而頭節點的執行緒釋放了同步狀態之後,將會喚醒其後繼節點,後繼節點的執行緒被喚醒後需要檢查自己的前驅節點是否是頭節點
  • 第二,維護同步佇列的FIFO原則

該方法中,節點自旋獲取同步狀態的行為如圖5-4所示

在圖5-4中,由於非首節點執行緒前驅節點出隊或者被中斷而從等待狀態返回,隨後檢查自己的前驅是否是頭節點,如果是則嘗試獲取同步狀態。可以看到節點和節點之間在迴圈檢查的過程中基本不相互通訊,而是簡單地判斷自己的前驅是否為頭節點,這樣就使得節點的釋放規則符合FIFO,並且也便於對過早通知的處理(過早通知是指前驅節點不是頭節點的執行緒由於中斷而被喚醒)。 獨佔式同步狀態獲取流程,也就是acquire(int arg)方法呼叫流程,如圖5-5所示:

3.共享式同步狀態獲取與釋放        共享式獲取與獨佔式獲取最主要的區別在於同一時刻能否有多個執行緒同時獲取到同步狀態。以檔案的讀寫為例,如果一個程式在對檔案進行讀操作,那麼這一時刻對於該檔案的寫操作均被阻塞,而讀操作能夠同時進行。 通過呼叫同步器的acquireShared(int arg)方法可以共享式地獲取同步狀態,該方法程式碼如下所示:

public final void acquireShared(int arg) {
	if (tryAcquireShared(arg) < 0)
		doAcquireShared(arg);
}
private void doAcquireShared(int arg) {
	final Node node = addWaiter(Node.SHARED);
	boolean failed = true;
	try {
		boolean interrupted = false;
		for (;;) {
			final Node p = node.predecessor();
			if (p == head) {
				int r = tryAcquireShared(arg);
				if (r >= 0) {
					setHeadAndPropagate(node, r);
					p.next = null; // help GC
					if (interrupted)
						selfInterrupt();
					failed = false;
					return;
				}
			}
			if (shouldParkAfterFailedAcquire(p, node) &&
				parkAndCheckInterrupt())
				interrupted = true;
		}
	} finally {
		if (failed)
			cancelAcquire(node);
	}
}

        在acquireShared(int arg)方法中,同步器呼叫tryAcquireShared(int arg)方法嘗試獲取同步狀態,tryAcquireShared(int arg)方法返回值為int型別,當返回值大於等於0時,表示能夠獲取到同步狀態。因此,在共享式獲取的自旋過程中,成功獲取到同步狀態並退出自旋的條件就是tryAcquireShared(int arg)方法返回值大於等於0可以看到,在doAcquireShared(int arg)方法的自旋過程中,如果當前節點的前驅為頭節點時,嘗試獲取同步狀態,如果返回值大於等於0,表示該次獲取同步狀態成功並從自旋過程中退出。        與獨佔式一樣,共享式獲取也需要釋放同步狀態,通過呼叫releaseShared(int arg)方法可以釋放同步狀態,該方法程式碼如下所示:

public final boolean releaseShared(int arg) {
	if (tryReleaseShared(arg)) {
		doReleaseShared();
		return true;
	}
	return false;
}

       該方法在釋放同步狀態之後,將會喚醒後續處於等待狀態的節點。對於能夠支援多個執行緒同時訪問的併發元件(比如Semaphore),它和獨佔式主要區別在於tryReleaseShared(int arg)方法必須確保同步狀態(或者資源數)執行緒安全釋放,一般是通過迴圈和CAS來保證的,因為釋放同步狀態的操作會同時來自多個執行緒。 4.獨佔式超時獲取同步狀態        通過呼叫同步器的doAcquireNanos(int arg,long nanosTimeout)方法可以超時獲取同步狀態,即在指定的時間段內獲取同步狀態,如果獲取到同步狀態則返回true,否則,返回false。該方法提供了傳統Java同步操作(比如synchronized關鍵字)所不具備的特性。        在分析該方法的實現前,先介紹一下響應中斷的同步狀態獲取過程。在Java 5之前,當一個執行緒獲取不到鎖而被阻塞在synchronized之外時,對該執行緒進行中斷操作,此時該執行緒的中斷標誌位會被修改,但執行緒依舊會阻塞在synchronized上,等待著獲取鎖。在Java 5中,同步器提供了acquireInterruptibly(int arg)方法,這個方法在等待獲取同步狀態時,如果當前執行緒被中斷,會立刻返回,並丟擲InterruptedException。        超時獲取同步狀態過程可以被視作響應中斷獲取同步狀態過程的“增強版”,doAcquireNanos(int arg,long nanosTimeout)方法在支援響應中斷的基礎上,增加了超時獲取的特性。針對超時獲取,主要需要計算出需要睡眠的時間間隔nanosTimeout,為了防止過早通知,nanosTimeout計算公式為:nanosTimeout-=now-lastTime,其中now為當前喚醒時間,lastTime為上次喚醒時間,如果nanosTimeout大於0則表示超時時間未到,需要繼續睡眠nanosTimeout納秒,反之,表示已經超時,該方法程式碼如下所示:

private boolean doAcquireNanos(int arg, long nanosTimeout)
		throws InterruptedException {
	if (nanosTimeout <= 0L)
		return false;
	final long deadline = System.nanoTime() + nanosTimeout;
	final Node node = addWaiter(Node.EXCLUSIVE);
	boolean failed = true;
	try {
		for (;;) {
			final Node p = node.predecessor();
			if (p == head && tryAcquire(arg)) {
				setHead(node);
				p.next = null; // help GC
				failed = false;
				return true;
			}
			nanosTimeout = deadline - System.nanoTime();
			if (nanosTimeout <= 0L)
				return false;
			if (shouldParkAfterFailedAcquire(p, node) &&
				nanosTimeout > spinForTimeoutThreshold)
				LockSupport.parkNanos(this, nanosTimeout);
			if (Thread.interrupted())
				throw new InterruptedException();
		}
	} finally {
		if (failed)
			cancelAcquire(node);
	}
}

       該方法在自旋過程中,當節點的前驅節點為頭節點時嘗試獲取同步狀態,如果獲取成功則從該方法返回,這個過程和獨佔式同步獲取的過程類似,但是在同步狀態獲取失敗的處理上有所不同。如果當前執行緒獲取同步狀態失敗,則判斷是否超時(nanosTimeout小於等於0表示已經超時),如果沒有超時,重新計算超時間隔nanosTimeout,然後使當前執行緒等待nanosTimeout納秒(當已到設定的超時時間,該執行緒會從LockSupport.parkNanos(Objectblocker,long nanos)方法返回)。        如果nanosTimeout小於等於spinForTimeoutThreshold(1000納秒)時,將不會使該執行緒進行超時等待,而是進入快速的自旋過程。原因在於,非常短的超時等待無法做到十分精確,如果這時再進行超時等待,相反會讓nanosTimeout的超時從整體上表現得反而不精確。因此,在超時非常短的場景下,同步器會進入無條件的快速自旋。獨佔式超時獲取同步態的流程如下所示

         從上圖可以看出,獨佔式超時獲取同步狀態doAcquireNanos(int arg,long nanosTimeout)和獨佔式獲取同步狀態acquire(int args)在流程上非常相似,其主要區別在於未獲取到同步狀態時的處理邏輯。acquire(int args)在未獲取到同步狀態時,將會使當前執行緒一直處於等待狀態,而doAcquireNanos(int arg,long nanosTimeout)會使當前執行緒等待nanosTimeout納秒,如果當前執行緒在nanosTimeout納秒內沒有獲取到同步狀態,將會從等待邏輯中自動返回。 5.自定義同步元件——TwinsLock

5.3 重入鎖

       重入鎖ReentrantLock,顧名思義,就是支援重進入的鎖,它表示該鎖能夠支援一個執行緒對資源的重複加鎖。除此之外,該鎖的還支援獲取鎖時的公平和非公平性選擇。ReentrantLock雖然沒能像synchronized關鍵字一樣支援隱式的重進入,但是在呼叫lock()方法時,已經獲取到鎖的執行緒,能夠再次呼叫lock()方法獲取鎖而不被阻塞。        這裡提到一個鎖獲取的公平性問題,如果在絕對時間上,先對鎖進行獲取的請求一定先被滿足,那麼這個鎖是公平的,反之,是不公平的。公平的獲取鎖,也就是等待時間最長的執行緒最優先獲取鎖,也可以說鎖獲取是順序的。ReentrantLock提供了一個建構函式,能夠控制鎖是否是公平的。事實上,公平的鎖機制往往沒有非公平的效率高,但是,並不是任何場景都是以TPS作為唯一的指標,公平鎖能夠減少“飢餓”發生的概率,等待越久的請求越是能夠得到優先滿足。 下面將著重分析ReentrantLock是如何實現重進入和公平性獲取鎖的特性,並通過測試來 驗證公平性獲取鎖對效能的影響。 1.實現重進入 重進入是指任意執行緒在獲取到鎖之後能夠再次獲取該鎖而不會被鎖所阻塞,該特性的實現需要解決以下兩個問題:

  1. 執行緒再次獲取鎖。鎖需要去識別獲取鎖的執行緒是否為當前佔據鎖的執行緒,如果是,則再次成功獲取
  2. 鎖的最終釋放。執行緒重複n次獲取了鎖,隨後在第n次釋放該鎖後,其他執行緒能夠獲取到該鎖。鎖的最終釋放要求鎖對於獲取進行計數自增,計數表示當前鎖被重複獲取的次數,而鎖被釋放時,計數自減,當計數等於0時表示鎖已經成功釋放

ReentrantLock是通過組合自定義同步器來實現鎖的獲取與釋放,以非公平性(預設的)實現為例,獲取同步狀態的程式碼如程式碼如下所示:

final boolean nonfairTryAcquire(int acquires) {
	final Thread current = Thread.currentThread();
	int c = getState();
	if (c == 0) {
		if (compareAndSetState(0, acquires)) {
			setExclusiveOwnerThread(current);
			return true;
		}
	}
	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;
}
	

該方法增加了再次獲取同步狀態的處理邏輯:通過判斷當前執行緒是否為獲取鎖的執行緒來決定獲取操作是否成功,如果是獲取鎖的執行緒再次請求,則將同步狀態值進行增加並返回true,表示獲取同步狀態成功。 成功獲取鎖的執行緒再次獲取鎖,只是增加了同步狀態值,這也就要求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;
}

如果該鎖被獲取了n次,那麼前(n-1)次tryRelease(int releases)方法必須返回false,而只有同步狀態完全釋放了,才能返回true。可以看到,該方法將同步狀態是否為0作為最終釋放的條件,當同步狀態為0時,將佔有執行緒設定為null,並返回true,表示釋放成功。 2.公平與非公平獲取鎖的區別        公平性與否是針對獲取鎖而言的,如果一個鎖是公平的,那麼鎖的獲取順序就應該符合請求的絕對時間順序,也就是FIFO。        回顧上一小節中介紹的nonfairTryAcquire(int acquires)方法,對於非公平鎖,只要CAS設定同步狀態成功,則表示當前執行緒獲取了鎖,而公平鎖則不同,如程式碼:

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;
}

該方法與nonfairTryAcquire(int acquires)比較,唯一不同的位置為判斷條件多了hasQueuedPredecessors()方法,即加入了同步佇列中當前節點是否有前驅節點的判斷,如果該方法返回true,則表示有執行緒比當前執行緒更早地請求獲取鎖,因此需要等待前驅執行緒獲取並釋放鎖之後才能繼續獲取鎖。

5.4 讀寫鎖

       之前提到鎖(如Mutex和ReentrantLock)基本都是排他鎖,這些鎖在同一時刻只允許一個執行緒進行訪問,而讀寫鎖在同一時刻可以允許多個讀執行緒訪問,但是在寫執行緒訪問時,所有的讀執行緒和其他寫執行緒均被阻塞。讀寫鎖維護了一對鎖,一個讀鎖和一個寫鎖,通過分離讀鎖和寫鎖,使得併發性相比一般的排他鎖有了很大提升。        除了保證寫操作對讀操作的可見性以及併發性的提升之外,讀寫鎖能夠簡化讀寫互動場景的程式設計方式。假設在程式中定義一個共享的用作快取資料結構,它大部分時間提供讀服務(例如查詢和搜尋),而寫操作佔有的時間很少,但是寫操作完成之後的更新需要對後續的讀服務可見。        在沒有讀寫鎖支援的(Java 5之前)時候,如果需要完成上述工作就要使用Java的等待通知機制,就是當寫操作開始時,所有晚於寫操作的讀操作均會進入等待狀態,只有寫操作完成並進行通知之後,所有等待的讀操作才能繼續執行(寫操作之間依靠synchronized關鍵進行同步),這樣做的目的是使讀操作能讀取到正確的資料,不會出現髒讀。改用讀寫鎖實現上述功能,只需要在讀操作時獲取讀鎖,寫操作時獲取寫鎖即可。當寫鎖被獲取到時,後續(非當前寫操作執行緒)的讀寫操作都會被阻塞,寫鎖釋放之後,所有操作繼續執行,程式設計方式相對於使用等待通知機制的實現方式而言,變得簡單明瞭。        一般情況下,讀寫鎖的效能都會比排它鎖好,因為大多數場景讀是多於寫的。在讀多於寫的情況下,讀寫鎖能夠提供比排它鎖更好的併發性和吞吐量。Java併發包提供讀寫鎖的實現是ReentrantReadWriteLock,它提供的特性如下所示:

5.4.1 讀寫鎖的介面與示例 ReadWriteLock僅定義了獲取讀鎖和寫鎖的兩個方法,即readLock()方法和writeLock()方法,而其實現——ReentrantReadWriteLock,除了介面方法之外,還提供了一些便於外界監控其內部工作狀態的方法,這些方法以及描述如下所示:

接下來,通過一個快取示例說明讀寫鎖的使用方式,示例程式碼如程式碼所示:  

public class Cache {
    private static final Map<String, Object>    map = new HashMap<String, Object>();
    private static final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    private static final Lock                   r   = rwl.readLock();
    private static final Lock                   w   = rwl.writeLock();

    public static final Object get(String key) {
        r.lock();
        try {
            return map.get(key);
        } finally {
            r.unlock();
        }
    }

    public static final Object put(String key, Object value) {
        w.lock();
        try {
            return map.put(key, value);
        } finally {
            w.unlock();
        }
    }

    public static final void clear() {
        w.lock();
        try {
            map.clear();
        } finally {
            w.unlock();
        }
    }
}

       上述示例中,Cache組合一個非執行緒安全的HashMap作為快取的實現,同時使用讀寫鎖的讀鎖和寫鎖來保證Cache是執行緒安全的。在讀操作get(String key)方法中,需要獲取讀鎖,這使得併發訪問該方法時不會被阻塞。寫操作put(String key,Object value)方法和clear()方法,在更新HashMap時必須提前獲取寫鎖,當獲取寫鎖後,其他執行緒對於讀鎖和寫鎖的獲取均被阻塞,而只有寫鎖被釋放之後,其他讀寫操作才能繼續。Cache使用讀寫鎖提升讀操作的併發性,也保證每次寫操作對所有的讀寫操作的可見性,同時簡化了程式設計方式。 5.4.2 讀寫鎖的實現分析        接下來分析ReentrantReadWriteLock的實現,主要包括:讀寫狀態的設計、寫鎖的獲取與釋放、讀鎖的獲取與釋放以及鎖降級(以下沒有特別說明讀寫鎖均可認為是ReentrantReadWriteLock)。 1.讀寫狀態的設計        讀寫鎖同樣依賴自定義同步器來實現同步功能,而讀寫狀態就是其同步器的同步狀態。回想ReentrantLock中自定義同步器的實現,同步狀態表示鎖被一個執行緒重複獲取的次數,而讀寫鎖的自定義同步器需要在同步狀態(一個整型變數)上維護多個讀執行緒和一個寫執行緒的狀態,使得該狀態的設計成為讀寫鎖實現的關鍵。        如果在一個整型變數上維護多種狀態,就一定需要“按位切割使用”這個變數,讀寫鎖將變數切分成了兩個部分,高16位表示讀,低16位表示寫,劃分方式如下所示:

       好好理解:當前同步狀態表示一個執行緒已經獲取了寫鎖,且重進入了兩次,同時也連續獲取了兩次讀鎖。讀寫鎖是如何迅速確定讀和寫各自的狀態呢? 答案是通過位運算。假設當前同步狀態值為S,寫狀態等於S&0x0000FFFF(將高16位全部抹去),讀狀態等於S>>>16(無符號補0右移16位)。當寫狀態增加1時,等於S+1,當讀狀態增加1時,等於S+(1<<16),也就是S+0x00010000。        根據狀態的劃分能得出一個推論:S不等於0時,當寫狀態(S&0x0000FFFF)等於0時,則讀狀態(S>>>16)大於0,即讀鎖已被獲取。 2.寫鎖的獲取與釋放 寫鎖是一個支援重進入的排它鎖。如果當前執行緒已經獲取了寫鎖,則增加寫狀態。如果當前執行緒在獲取寫鎖時,讀鎖已經被獲取(讀狀態不為0)或者該執行緒不是已經獲取寫鎖的執行緒,則當前執行緒進入等待狀態,獲取寫鎖的程式碼如程式碼所示:

  protected final boolean tryAcquire(int acquires) {
	Thread current = Thread.currentThread();
	int c = getState();
	int w = exclusiveCount(c);
	if (c != 0) {
		// (Note: if c != 0 and w == 0 then shared count != 0)
		//讀鎖已經被獲取(讀狀態不為0)或者該執行緒不是已經獲取寫鎖的執行緒
		if (w == 0 || current != getExclusiveOwnerThread())
			return false;
		if (w + exclusiveCount(acquires) > MAX_COUNT)
			throw new Error("Maximum lock count exceeded");
		// Reentrant acquire
		setState(c + acquires);
		return true;
	}
	if (writerShouldBlock() ||
		!compareAndSetState(c, c + acquires))
		return false;
	setExclusiveOwnerThread(current);
	return true;
}

       該方法除了重入條件(當前執行緒為獲取了寫鎖的執行緒)之外,增加了一個讀鎖是否存在的判斷。如果存在讀鎖,則寫鎖不能被獲取,原因在於:讀寫鎖要確保寫鎖的操作對讀鎖可見,如果允許讀鎖在已被獲取的情況下對寫鎖的獲取,那麼正在執行的其他讀執行緒就無法感知到當前寫執行緒的操作。因此,只有等待其他讀執行緒都釋放了讀鎖,寫鎖才能被當前執行緒獲取,而寫鎖一旦被獲取,則其他讀寫執行緒的後續訪問均被阻塞。        寫鎖的釋放與ReentrantLock的釋放過程基本類似,每次釋放均減少寫狀態,當寫狀態為0時表示寫鎖已被釋放,從而等待的讀寫執行緒能夠繼續訪問讀寫鎖,同時前次寫執行緒的修改對後續讀寫執行緒可見 3.讀鎖的獲取與釋放 讀鎖是一個支援重進入的共享鎖,它能夠被多個執行緒同時獲取,在沒有其他寫執行緒訪問(或者寫狀態為0)時,讀鎖總會被成功地獲取,而所做的也只是(執行緒安全的)增加讀狀態。如果當前執行緒已經獲取了讀鎖,則增加讀狀態。如果當前執行緒在獲取讀鎖時,寫鎖已被其他執行緒獲取,則進入等待狀態。獲取讀鎖的實現從Java 5到Java 6變得複雜許多,主要原因是新增了一些功能,例如getReadHoldCount()方法,作用是返回當前執行緒獲取讀鎖的次數。讀狀態是所有執行緒獲取讀鎖次數的總和,而每個執行緒各自獲取讀鎖的次數只能選擇儲存在ThreadLocal中,由執行緒自身維護,這使獲取讀鎖的實現變得複雜。因此,這裡將獲取讀鎖的程式碼做了刪減,保留必要的部分,如程式碼:

protected final int tryAcquireShared(int unused) {
	for (;;) {
		int c = getState();
		int nextc = c + (1 << 16);
		if (nextc < c)
			throw new Error("Maximum lock count exceeded");
		if (exclusiveCount(c) != 0 && owner != Thread.currentThread())
			return -1;
		if (compareAndSetState(c, nextc))
			return 1;
	}
}

在tryAcquireShared(int unused)方法中,如果其他執行緒已經獲取了寫鎖,則當前執行緒獲取讀鎖失敗,進入等待狀態。如果當前執行緒獲取了寫鎖或者寫鎖未被獲取,則當前執行緒(執行緒安全,依靠CAS保證)增加讀狀態,成功獲取讀鎖。讀鎖的每次釋放(執行緒安全的,可能有多個讀執行緒同時釋放讀鎖)均減少讀狀態,減少的值是(1<<16)。 4.鎖降級 鎖降級指的是寫鎖降級成為讀鎖。如果當前執行緒擁有寫鎖,然後將其釋放,最後再獲取讀鎖,這種分段完成的過程不能稱之為鎖降級。鎖降級是指把持住(當前擁有的)寫鎖,再獲取到讀鎖,隨後釋放(先前擁有的)寫鎖的過程。

5.5 LockSupport工具  

當需要阻塞或喚醒一個執行緒的時候,都會使用LockSupport工具類來完成相應工作。LockSupport定義了一組的公共靜態方法,這些方法提供了最基本的執行緒阻塞和喚醒功能,而LockSupport也成為構建同步元件的基礎工具。LockSupport定義了一組以park開頭的方法用來阻塞當前執行緒,以及unpark(Thread thread)方法來喚醒一個被阻塞的執行緒:

5.6 Condition介面  

任意一個Java物件,都擁有一組監視器方法(定義在java.lang.Object上),主要包括wait()、wait(long timeout)、notify()以及notifyAll()方法,這些方法與synchronized同步關鍵字配合,可以實現等待/通知模式。Condition介面也提供了類似Object的監視器方法,與Lock配合可以實現等待/通知模式,但是這兩者在使用方式以及功能特性上還是有差別的。通過對比Object的監視器方法和Condition介面,可以更詳細地瞭解Condition的特性:

5.6.1 Condition介面與示例 Condition定義了等待/通知兩種型別的方法,當前執行緒呼叫這些方法時,需要提前獲取到Condition物件關聯的鎖。Condition物件是由Lock物件(呼叫Lock物件的newCondition()方法)創建出來的,換句話說,Condition是依賴Lock物件的。Condition的使用方式比較簡單,需要注意在呼叫方法前獲取鎖,使用方式如程式碼所示:

public class ConditionUseCase {
    Lock      lock      = new ReentrantLock();
    Condition condition = lock.newCondition();

    public void conditionWait() throws InterruptedException {
        lock.lock();
        try {
            condition.await();
        } finally {
            lock.unlock();
        }
    }

    public void conditionSignal() throws InterruptedException {
        lock.lock();
        try {
            condition.signal();
        } finally {
            lock.unlock();
        }
    }
}

5.6.2 Condition的實現分析

ConditionObject是同步器AbstractQueuedSynchronizer的內部類,因為Condition的操作需要獲取相關聯的鎖,所以作為同步器的內部類也較為合理。每個Condition物件都包含著一個佇列(以下稱為等待佇列),該佇列是Condition物件實現等待/通知功能的關鍵。下面將分析Condition的實現,主要包括:等待佇列、等待和通知,下面提到的Condition如果不加說明均指的是ConditionObject。 1.等待佇列

2.等待 3.通知  

5.3 重入鎖 5.4 讀寫鎖 5.5 LockSupport工具 5.6 Condition介面 5.7 小結