原創文章&經驗總結&從校招到 A 廠一路陽光一路滄桑
詳情請戳www.codercc.com
1. ArrayBlockingQueue 簡介
在多執行緒程式設計過程中,為了業務解耦和架構設計,經常會使用併發容器用於儲存多執行緒間的共享資料,這樣不僅可以保證執行緒安全,還可以簡化各個執行緒操作。例如在“生產者-消費者”問題中,會使用阻塞佇列(BlockingQueue)作為資料容器,關於 BlockingQueue 可以看這篇文章。為了加深對阻塞佇列的理解,唯一的方式是對其實驗原理進行理解,這篇文章就主要來看看 ArrayBlockingQueue 和 LinkedBlockingQueue 的實現原理。
2. ArrayBlockingQueue 實現原理
阻塞佇列最核心的功能是,能夠可阻塞式的插入和刪除佇列元素。當前佇列為空時,會阻塞消費資料的執行緒,直至佇列非空時,通知被阻塞的執行緒;當佇列滿時,會阻塞插入資料的執行緒,直至佇列未滿時,通知插入資料的執行緒(生產者執行緒)。那麼,多執行緒中訊息通知機制最常用的是 lock 的 condition 機制,關於 condition 可以看這篇文章的詳細介紹。那麼 ArrayBlockingQueue 的實現是不是也會採用 Condition 的通知機制呢?下面來看看。
2.1 ArrayBlockingQueue 的主要屬性
ArrayBlockingQueue 的主要屬性如下:
/** The queued items */ final Object[] items;
/** items index for next take, poll, peek or remove */ int takeIndex;
/** items index for next put, offer, or add */ int putIndex;
/** Number of elements in the queue */ int count;
/*
- Concurrency control uses the classic two-condition algorithm
- found in any textbook. */
/** Main lock guarding all access */ final ReentrantLock lock;
/** Condition for waiting takes */ private final Condition notEmpty;
複製程式碼
/** Condition for waiting puts */ private final Condition notFull; 複製程式碼
從原始碼中可以看出 ArrayBlockingQueue 內部是採用陣列進行資料儲存的(屬性items
),為了保證執行緒安全,採用的是ReentrantLock lock
,為了保證可阻塞式的插入刪除資料利用的是 Condition,當獲取資料的消費者執行緒被阻塞時會將該執行緒放置到 notEmpty 等待佇列中,當插入資料的生產者執行緒被阻塞時,會將該執行緒放置到 notFull 等待佇列中。而 notEmpty 和 notFull 等中要屬性在構造方法中進行建立:
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
複製程式碼
接下來,主要看看可阻塞式的 put 和 take 方法是怎樣實現的。
2.2 put 方法詳解
put(E e)
方法原始碼如下:
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
//如果當前佇列已滿,將執行緒移入到notFull等待佇列中
while (count == items.length)
notFull.await();
//滿足插入資料的要求,直接進行入隊操作
enqueue(e);
} finally {
lock.unlock();
}
}
複製程式碼
該方法的邏輯很簡單,當佇列已滿時(count == items.length
)將執行緒移入到 notFull 等待佇列中,如果當前滿足插入資料的條件,就可以直接呼叫enqueue(e)
插入資料元素。enqueue 方法原始碼為:
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
//插入資料
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
//通知消費者執行緒,當前佇列中有資料可供消費
notEmpty.signal();
}
複製程式碼
enqueue 方法的邏輯同樣也很簡單,先完成插入資料,即往陣列中新增資料(items[putIndex] = x
),然後通知被阻塞的消費者執行緒,當前佇列中有資料可供消費(notEmpty.signal()
)。
2.3 take 方法詳解
take 方法原始碼如下:
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
//如果佇列為空,沒有資料,將消費者執行緒移入等待佇列中
while (count == 0)
notEmpty.await();
//獲取資料
return dequeue();
} finally {
lock.unlock();
}
}
複製程式碼
take 方法也主要做了兩步:1. 如果當前佇列為空的話,則將獲取資料的消費者執行緒移入到等待佇列中;2. 若佇列不為空則獲取資料,即完成出隊操作dequeue
。dequeue 方法原始碼為:
private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
@SuppressWarnings("unchecked")
//獲取資料
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
//通知被阻塞的生產者執行緒
notFull.signal();
return x;
}
複製程式碼
dequeue 方法也主要做了兩件事情:1. 獲取佇列中的資料,即獲取陣列中的資料元素((E) items[takeIndex]
);2. 通知 notFull 等待佇列中的執行緒,使其由等待佇列移入到同步佇列中,使其能夠有機會獲得 lock,並執行完成功退出。
從以上分析,可以看出 put 和 take 方法主要是通過 condition 的通知機制來完成可阻塞式的插入資料和獲取資料。在理解 ArrayBlockingQueue 後再去理解 LinkedBlockingQueue 就很容易了。
3. LinkedBlockingQueue 實現原理
LinkedBlockingQueue 是用連結串列實現的有界阻塞佇列,當構造物件時為指定佇列大小時,佇列預設大小為Integer.MAX_VALUE
。從它的構造方法可以看出:
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
複製程式碼
3.1 LinkedBlockingQueue 的主要屬性
LinkedBlockingQueue 的主要屬性有:
/** Current number of elements */ private final AtomicInteger count = new AtomicInteger();
/**
- Head of linked list.
- Invariant: head.item == null */ transient Node<E> head;
/**
- Tail of linked list.
- Invariant: last.next == null */ private transient Node<E> last;
/** Lock held by take, poll, etc */ private final ReentrantLock takeLock = new ReentrantLock();
/** Wait queue for waiting takes */ private final Condition notEmpty = takeLock.newCondition();
/** Lock held by put, offer, etc */ private final ReentrantLock putLock = new ReentrantLock();
複製程式碼
/** Wait queue for waiting puts */ private final Condition notFull = putLock.newCondition(); 複製程式碼
可以看出與 ArrayBlockingQueue 主要的區別是,LinkedBlockingQueue 在插入資料和刪除資料時分別是由兩個不同的 lock(takeLock
和putLock
)來控制執行緒安全的,因此,也由這兩個 lock 生成了兩個對應的 condition(notEmpty
和notFull
)來實現可阻塞的插入和刪除資料。並且,採用了連結串列的資料結構來實現佇列,Node 結點的定義為:
static class Node<E> { E item;
複製程式碼/** * One of: * - the real successor Node * - this Node, meaning the successor is head.next * - null, meaning there is no successor (this is the last node) */ Node<E> next; Node(E x) { item = x; } 複製程式碼
} 複製程式碼
接下來,我們也同樣來看看 put 方法和 take 方法的實現。
3.2 put 方法詳解
put 方法原始碼為:
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
// Note: convention in all put/take/etc is to preset local var
// holding count negative to indicate failure unless set.
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
/*
* Note that count is used in wait guard even though it is
* not protected by lock. This works because count can
* only decrease at this point (all other puts are shut
* out by lock), and we (or some other waiting put) are
* signalled if it ever changes from capacity. Similarly
* for all other uses of count in other wait guards.
*/
//如果佇列已滿,則阻塞當前執行緒,將其移入等待佇列
while (count.get() == capacity) {
notFull.await();
}
//入隊操作,插入資料
enqueue(node);
c = count.getAndIncrement();
//若佇列滿足插入資料的條件,則通知被阻塞的生產者執行緒
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
}
複製程式碼
put 方法的邏輯也同樣很容易理解,可見註釋。基本上和 ArrayBlockingQueue 的 put 方法一樣。take 方法的原始碼如下:
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
//當前佇列為空,則阻塞當前執行緒,將其移入到等待佇列中,直至滿足條件
while (count.get() == 0) {
notEmpty.await();
}
//移除隊頭元素,獲取資料
x = dequeue();
c = count.getAndDecrement();
//如果當前滿足移除元素的條件,則通知被阻塞的消費者執行緒
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
複製程式碼
take 方法的主要邏輯請見於註釋,也很容易理解。
4. ArrayBlockingQueue 與 LinkedBlockingQueue 的比較
相同點:ArrayBlockingQueue 和 LinkedBlockingQueue 都是通過 condition 通知機制來實現可阻塞式插入和刪除元素,並滿足執行緒安全的特性;
不同點:1. ArrayBlockingQueue 底層是採用的陣列進行實現,而 LinkedBlockingQueue 則是採用連結串列資料結構;
ArrayBlockingQueue 插入和刪除資料,只採用了一個 lock,而 LinkedBlockingQueue 則是在插入和刪除分別採用了 putLock
和takeLock
,這樣可以降低執行緒由於執行緒無法獲取到 lock 而進入 WAITING 狀態的可能性,從而提高了執行緒併發執行的效率。