思維導圖:
引言:
當原生的同步工具類,比如CountDownLauch,Semaphore,ReentrantLock等不能提供我們所需要的功能時,我們就需要自定義一個新的同步工具類了。
本文將會先介紹如何對狀態依賴性進行管理,然後介紹自定義同步類的工具體系。
- 原理部分:如何對具有狀態依賴性的同步工具進行管理
- 體系部分:介紹java提供的底層的同步體系
一.狀態依賴性的管理
什麼是狀態依賴性呢?比如阻塞隊列獲取元素時,隊列中必須要有元素,隊列要有元素就是一個狀態依賴。再比如,我們不能從一個爲空的阻塞隊列中刪除元素。這就是狀態依賴性。
那麼,當我們構建自定義的同步工具是,如何處理這種狀態依賴性呢?
現在,我們構建一個實現有界緩存的基類,可以put或者take,我們在以下幾個小節中逐漸完善它。
@ThreadSafe
public abstract class BaseBoundedBuffer <V> {
@GuardedBy("this") private final V[] buf;
@GuardedBy("this") private int tail;
@GuardedBy("this") private int head;
@GuardedBy("this") private int count;
protected BaseBoundedBuffer(int capacity) {
this.buf = (V[]) new Object[capacity];
}
protected synchronized final void doPut(V v) {
buf[tail] = v;
if (++tail == buf.length) {
tail = 0;
}
++count;
}
protected synchronized final V doTake() {
V v = buf[head];
buf[head] = null;
if (++head == buf.length) {
head = 0;
}
--count;
return v;
}
public synchronized final boolean isFull() {
return count == buf.length;
}
public synchronized final boolean isEmpty() {
return count == 0;
}
}
1.1 傳遞失敗調用
當對象的狀態不符合我們當前方法的期待時,我們可以將失敗的調用傳遞給上層調用者。例如,我們可以拋出一個異常。
但是,如果這樣處理的話,上層調用者就必須捕獲異常或者進行其他的什麼判斷。
@ThreadSafe
public class GrumpyBoundedBuffer <V> extends BaseBoundedBuffer<V> {
public GrumpyBoundedBuffer() {
this(100);
}
public GrumpyBoundedBuffer(int size) {
super(size);
}
public synchronized void put(V v) throws BufferFullException {
if (isFull()) {
throw new BufferFullException();
}
doPut(v);
}
public synchronized V take() throws BufferEmptyException {
if (isEmpty()) {
throw new BufferEmptyException();
}
return doTake();
}
}
1.2 利用輪詢和睡眠實現阻塞
我們不能總是讓調用方來處理狀態依賴性的異常,最好的辦法是,當隊列中沒有元素又需要take時,保持阻塞,直到有元素才處理,或者當隊列已滿但又需要put時,保持阻塞,直到隊列有空閒位置時才處理。
下面,我們將利用輪詢和睡眠來實現簡單的阻塞。
@ThreadSafe
public class SleepyBoundedBuffer <V> extends BaseBoundedBuffer<V> {
int SLEEP_GRANULARITY = 60;
public SleepyBoundedBuffer() {
this(100);
}
public SleepyBoundedBuffer(int size) {
super(size);
}
public void put(V v) throws InterruptedException {
while (true) {
synchronized (this) {
if (!isFull()) {
doPut(v);
return;
}
}
Thread.sleep(SLEEP_GRANULARITY);
}
}
public V take() throws InterruptedException {
while (true) {
synchronized (this) {
if (!isEmpty()) {
return doTake();
}
}
Thread.sleep(SLEEP_GRANULARITY);
}
}
}
1.3 使用條件隊列阻塞
使用輪詢和休眠固然可以實現簡單的休眠,但是這樣做有一個較大的缺點,那就是,如果在休眠期內,狀態依賴發生了改變,那將不會做出任何響應。如何是得掛起的線程在狀態依賴改變後立即醒來呢?
那麼如何解決上述問題呢?答案就是使用java的條件隊裏。所有的隊列都是條件隊列,隊列中儲存的是希望關注此對象的線程們。如果要使用某個對象的條件隊列,必須先獲取此對象的獨佔鎖。然後調用條件隊列的API,
- wait:自動釋放鎖,並請求操作系統掛起當前線程
- notify:隨機通知並環境條件隊列中的一個線程
- notifyAll :喚醒所有條件隊列中的線程
下面,我麼使用條件隊列實現這個有界隊列。
@ThreadSafe
public class BoundedBuffer <V> extends BaseBoundedBuffer<V> {
// 條件謂詞: not-full (!isFull())
// 條件謂詞: not-empty (!isEmpty())
public BoundedBuffer() {
this(100);
}
public BoundedBuffer(int size) {
super(size);
}
// 阻塞並直到: not-full
public synchronized void put(V v) throws InterruptedException {
while (isFull()) {
wait();
}
doPut(v);
notifyAll();
}
// 阻塞並直到: not-empty
public synchronized V take() throws InterruptedException {
while (isEmpty()) {
wait();
}
V v = doTake();
notifyAll();
return v;
}
}
二.底層工具
這個小節我們將介紹幾種實現同步工具類所依賴的幾種比較底層的工具。
2.1 條件隊列
在上文中,我們使用條件隊列實現了阻塞功能。但是,使用條件隊列還有一些其他的問題值得我們關注
- 條件謂詞:條件謂詞就是使某個操作稱爲狀態依賴的前提條件。想要正確的使用條件隊列,關鍵是找出對象在哪個條件謂詞上等待
- 過早喚醒:當某個線程被notifyAll或者notify喚醒時,並不代表條件謂詞已經成真了,所以,我們必須將wait方在一個while循環中,保證只有條件謂詞爲真時,才往下執行代碼
- 丟失的信號:指的是線程必須等待一個已經爲真的信號,但是在開始等待之前沒有檢查條件謂詞。所以,我們最好在方法執行開始時就對條件謂詞進行檢查。
- 通知:notify只會隨機通知一個線程,很有可能通知的不是需要此條件謂詞的線程。而真正需要判斷此條件謂詞是否變化的線程有沒有通知到,就會發生類似信號丟失的情況,所以,最好使用notifyAll,儘管喚醒所有線程會有一定的性能開銷。
- 子類的安全問題:對於具有狀態依賴性處理的類,要麼完全關閉方法的繼承,有麼將條件謂詞和通知協議完全公開。
- 入口協和和出口協議:對於每個依賴狀態的操,以及每個修改其他操作所依賴的狀態的操作,都應該定義一個入口協議和出口協議,入口協議就是當前操作的條件謂詞,出口協議則包括檢查被該操作修改的狀態變量,並確認是否使某個條件謂詞爲真,若是,則通知相關條件隊列。
2.2 顯式的Condition對象
條件隊列具有顯式的對象Condition,與對象內置的條件隊列不同只和對象的內置鎖對應不同,一個顯式鎖Lock可以和多個Condition對象對應,也會提供比內置條件隊列更多的功能。
請注意,Condition使用signal()替代notify(),使用signalAll()替代notifyAll,但是由於所有的對象都是從Object繼承的,所以本身也有notify和notifyAll的方法可供調用,所以使用時務必小心。
下面,我們使用顯式的條件隊列實現有界隊列:
@ThreadSafe
public class ConditionBoundedBuffer <T> {
protected final Lock lock = new ReentrantLock();
// 條件謂詞: notFull (count < items.length)
private final Condition notFull = lock.newCondition();
// 條件謂詞: notEmpty (count > 0)
private final Condition notEmpty = lock.newCondition();
private static final int BUFFER_SIZE = 100;
@GuardedBy("lock") private final T[] items = (T[]) new Object[BUFFER_SIZE];
@GuardedBy("lock") private int tail, head, count;
// 阻塞並直到: notFull
public void put(T x) throws InterruptedException {
lock.lock();
try {
while (count == items.length) {
notFull.await();
}
items[tail] = x;
if (++tail == items.length) {
tail = 0;
}
++count;
notEmpty.signal();
} finally {
lock.unlock();
}
}
// 阻塞並直到: notEmpty
public T take() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmpty.await();
}
T x = items[head];
items[head] = null;
if (++head == items.length) {
head = 0;
}
--count;
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}
2.3 AbstractQueueSynchronizer
我們簡稱AbstractQueueSynchronizer爲AQS。如果一個類想成爲狀態依賴的類,就必須擁有一些狀態。AQS就負責管理同步器類中的狀態,它管理了一個整數狀態信息,可以通過getState,setState,compareAndSetState等protected類型的方法進行操作。
許多的同步器類都是基於AQS實現的,比如ReetrantReadWriteLock,TeentrantLock,CountDownLuach,Semaphore,FutrueTask。他們都利用AQS所管理的整數狀態信息充當自己的狀態。
- TeentrantLock中用於保存獲取鎖的次數。
- Semaphore中用於保存當前可用許可的數量。
- CountDownLatch中用於保存當前的計數值。
- FutrueTask中用於保存任務的狀態
- ReetrantReadWriteLock中使用qian16位和後16位分別保存寫入鎖和讀取鎖的計數
下面,我們基於AQS實現一個簡單的二元鎖:
@ThreadSafe
public class OneShotLatch {
private final Sync sync = new Sync();
public void signal() {
sync.releaseShared(0);
}
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(0);
}
private class Sync extends AbstractQueuedSynchronizer {
@Override
protected int tryAcquireShared(int ignored) {
// 如果閉鎖是開的 (state == 1), 那麼操作成功,否則失敗
return (getState() == 1) ? 1 : -1;
}
@Override
protected boolean tryReleaseShared(int ignored) {
// 現在打開閉鎖
setState(1);
// 現在其他的線程可以獲取該閉鎖
return true;
}
}
}