創造し続け、成長を加速!「ナゲッツデイリー新プラン・10月アップデートチャレンジ」参加4日目、イベント詳細はこちら
普段の開発ではBlockingQueueを使うことは少ないようで、例えばデータのセットを保存したい場合はArrayList、キーと値のペアのデータを保存したい場合はHashMapを使います。 BlockingQueue を使用する必要がありますか?
1. BlockingQueue の適用シナリオ
データのバッチを処理した後、このデータのバッチを下流のメソッドに送信してさらに処理する必要がありますが、下流のメソッドの処理速度は制御されておらず、高速または低速になる場合があります。ダウンストリーム メソッドの処理速度が遅く、現在のメソッドの処理速度が遅くなる場合、この時点でどうすればよいですか?
解決策としてスレッドプールを利用することを考えるかもしれませんが、大量のスレッドを作成する必要があり、下流のメソッドが同時実行をサポートしていないことを考慮すると、CPU を集中的に使用するタスクの場合、マルチスレッド処理が遅くなる可能性があります。頻繁なコンテキストの切り替えが必要なため、シングルスレッド処理よりも。
この時点で、BlockingQueue の使用を検討できます. BlockingQueue の最も典型的なアプリケーション シナリオは、上記のプロデューサー/コンシューマー モデルです。プロデューサはデータをキューに入れ、コンシューマはキューからデータを取得し、途中で BlockingQueue をバッファ キューとして使用します。これにより、プロデューサとコンシューマの間の非同期速度の問題が解決されます。
メッセージ キュー (MessageQueue) について考えることができます。メッセージ キューは分散ブロッキング キューに相当し、BlockingQueue はこのマシンでのみ機能するローカル ブロッキング キューに相当します。分散キャッシュ(Redis、Memcacheなど)、ローカルキャッシュ(Guava、Caffeineなど)に対応。
また、多くのフレームワークには BlockingQueue の影があり、たとえば、スレッド プールでは、BlockingQueue はタスクのバッファリングに使用されます。メッセージ キューでメッセージを送信およびプルするメソッドも BlockingQueue から借用されており、使用方法は非常に似ています。
今日は、Queue の基礎となるソース コードを詳細に分析しましょう。
2. BlockingQueue の使い方
BlockingQueue の使用法は非常に単純です。つまり、データを入れてデータを取得します。
/**
* @apiNote BlockingQueue示例
* @author 一灯架构
*/
public class Demo {
public static void main(String[] args) throws InterruptedException {
// 1. 创建队列,设置容量是10
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
// 2. 往队列中放数据
queue.put(1);
// 3. 从队列中取数据
Integer result = queue.take();
}
}
复制代码
さまざまな使用シナリオに対応するために、BlockingQueue はデータを挿入および削除するための多くのメソッドを設計しました。
操作する | 例外をスローする | 特定の値を返す | ブロック | しばらくブロック |
---|---|---|---|---|
データを置く | add |
offer |
put |
offer(e, time, unit) |
データを取得する | remove |
poll |
take |
poll(time, unit) |
データを取得する (削除しないでください) | element() |
peek() |
サポートしません | サポートしません |
これらのメソッド グループの違いは次のとおりです。
- キューがいっぱいになり、データをキューに入れると、add メソッドは例外をスローし、offer メソッドは false を返し、put メソッドは (別のスレッドがキューからデータを取得するまで) ブロックし続けます。時刻を指定してから false を返します。
- キューが空でキューからデータが取得されると、remove メソッドは例外をスローし、poll メソッドは null を返し、take メソッドは (他のスレッドがデータをキューに入れるまで) ブロックし、poll メソッドは指定された時間ブロックします。その後、null を返します。
- キューが空の場合、(データを削除せずに) キューに移動してデータを表示すると、要素メソッドは例外をスローし、peek メソッドは null を返します。
仕事で最もよく使われるメソッドは、指定された時間ブロックする offer と poll のメソッドです。
3. BlockingQueue 実装クラス
BlockingQueue には、主にアプリケーション シナリオが異なるため、一般的に次の 5 つの実装クラスがあります。
-
ArrayBlockingQueue
配列に基づいて実装されたブロッキング キューは、バインドされたキューであるキューを作成するときに容量サイズを指定する必要があります。
-
LinkedBlockingQueue
リンクされたリストに基づいて実装されたブロッキング キュー。デフォルトは無制限のキューで、容量は作成時に指定できます。
-
同期キュー
バッファリングされていないブロッキング キュー。生成されたデータはすぐに消費する必要があります
-
PriorityBlockingQueue
優先度のあるブロッキング キューが実装され、データ表示に基づいて、無制限のキューです
-
DelayQueue
PriorityQueue に基づいて遅延機能を実装するブロッキング キューは、無制限のキューです。
4. BlockingQueue のソースコード分析
BlockingQueue の 5 つのサブクラスも同様に実装されていますが、今回は、最も一般的に使用されている ArrayBlockingQueue をソース コードの解析に使用します。
4.1 ArrayBlockingQueue クラスのプロパティ
ArrayBlockingQueue クラスに含まれるプロパティを見てみましょう。
// 用来存放数据的数组
final Object[] items;
// 下次取数据的数组下标位置
int takeIndex;
// 下次放数据的数组下标位置
int putIndex;
// 当前已有元素的个数
int count;
// 独占锁,用来保证存取数据安全
final ReentrantLock lock;
// 取数据的条件
private final Condition notEmpty;
// 放数据的条件
private final Condition notFull;
复制代码
ArrayBlockingQueue の 4 つのグループのデータ アクセス メソッドの実装も同様であり、今回は put メソッドと take メソッドを使用して分析します。
4.2 put メソッドのソースコード解析
データを入れる場合も、データを取得する場合も、行の先頭から開始し、徐々に行の末尾に移動します。
// 放数据,如果队列已满,就一直阻塞,直到有其他线程从队列中取走数据
public void put(E e) throws InterruptedException {
// 校验元素不能为空
checkNotNull(e);
final ReentrantLock lock = this.lock;
// 加锁,加可中断的锁
lock.lockInterruptibly();
try {
// 如果队列已满,就一直阻塞,直到被唤醒
while (count == items.length)
notFull.await();
// 如果队列未满,就往队列添加元素
enqueue(e);
} finally {
// 结束后,别忘了释放锁
lock.unlock();
}
}
// 实际往队列添加数据的方法
private void enqueue(E x) {
// 获取数组
final Object[] items = this.items;
// putIndex 表示本次插入的位置
items[putIndex] = x;
// ++putIndex 计算下次插入的位置
// 如果下次插入的位置,正好等于队尾,下次插入就从 0 开始
if (++putIndex == items.length)
putIndex = 0;
// 元素数量加一
count++;
// 唤醒因为队列空等待的线程
notEmpty.signal();
}
复制代码
ソースコードに興味深い設計があり、要素を追加するときにキューの最後に到達すると、次はキューの先頭から追加されます。これは循環キューを作成することと同じです。
次のように:
4.3 takeメソッドのソースコード
// 取数据,如果队列为空,就一直阻塞,直到有其他线程往队列中放数据
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
// 加锁,加可中断的锁
lock.lockInterruptibly();
try {
// 如果队列为空,就一直阻塞,直到被唤醒
while (count == 0)
notEmpty.await();
// 如果队列不为空,就从队列取数据
return dequeue();
} finally {
// 结束后,别忘了释放锁
lock.unlock();
}
}
// 实际从队列取数据的方法
private E dequeue() {
// 获取数组
final Object[] items = this.items;
// takeIndex 表示本次取数据的位置,是上一次取数据时计算好的
E x = (E) items[takeIndex];
// 取完之后,就把队列该位置的元素删除
items[takeIndex] = null;
// ++takeIndex 计算下次拿数据的位置
// 如果正好等于队尾的话,下次就从 0 开始拿数据
if (++takeIndex == items.length)
takeIndex = 0;
// 元素数量减一
count--;
if (itrs != null)
itrs.elementDequeued();
// 唤醒被队列满所阻塞的线程
notFull.signal();
return x;
}
复制代码
4.4 まとめ
- ArrayBlockingQueue は、配列に基づいて実装されたブロッキング キューです. キューを作成するときに、容量とサイズを指定する必要があります. これは境界付きキューです.
- ArrayBlockingQueue の最下層は、配列の位置を再利用できるようにするための循環キューの形式になっています。
- ArrayBlockingQueue アクセスは ReentrantLock によってロックされ、スレッドの安全性が確保され、マルチスレッド環境で安心して使用できます。
- ArrayBlockingQueue を使用する場合は、プロデューサーとコンシューマーのレートが一致するようにキューの長さを見積もります。
私は "One Light Architecture" です. この記事が参考になった場合は、お友達に「いいね」、「コメント」、「注意を払う」ことを歓迎します. 古いアイアンに感謝します. 次号でお会いしましょう