Javaソースコード分析とインタビューの質問-ArrayBlockingQueueソースコード分析

関連するブログ、ムークラス参照列この一連のJavaソースコードとシステムメーカーは簡潔Zhentiインタビュアー
この列の下には、GitHubのアドレスです:
ソースは解決:https://github.com/luanqiu/java8
記事デモ:HTTPS:// GitHubの。 com / luanqiu / java8_demo
クラスメートは必要に応じてそれを見ることができます)

Javaソースコード分析とインタビューの質問-ArrayBlockingQueueソースコード分析

入門フレーズ
我々は章を紹介するこのセクションでは、キューで締めくくら:ArrayBlockingQueue。文字通りの翻訳によると、中国語は配列ブロッキングキューと呼ばれ、その名前から、このブロッキングキューは下部の配列を使用することがわかります。配列に関して言えば、ArrayListとHashMapを考えることができます。新しいシナリオでは、ArrayListはサイズ++を通じて新しい配列の添え字の位置を見つけます。HashMapはハッシュアルゴリズムによって添え字の位置を計算するので、ArrayBlockingQueueは同じですか?どんな方法?いいえ、ArrayBlockingQueueは非常に素晴らしい方法を使用しています。

説明の便宜上、チームの頭は配列の頭であり、チームの尾は配列の尾です。

1全体的なアーキテクチャ

クラスのコメントからいくつかの有用な情報を得ることができます:

1.1クラスのメモ

  1. 境界ブロックアレイ。容量が作成されると、後続のサイズは変更できません。
  2. 要素は順序付けられ、先入れ先出し法に従って並べ替えられ、チームの終わりからデータデータが挿入され、チームのヘッドからデータが取得されます。
  3. キューがいっぱいになると、キュー内の書き込みデータがブロックされ、キューが空になると、キュー内のデータもブロックされます。

クラスノートから、ArrayBlockingQueueは一般的な配列構造クラスと同じではなく、動的に拡張することはできません。キューがいっぱいまたは空の場合、テイクアンドプットはブロックされます。

1.2データ構造

// 队列存放在 object 的数组里面
// 数组大小必须在初始化的时候手动设置,没有默认大小
final Object[] items;
 
// 下次拿数据的时候的索引位置
int takeIndex;
 
// 下次放数据的索引位置
int putIndex;
 
// 当前已有元素的大小
int count;
 
// 可重入的锁
final ReentrantLock lock;
 
// take的队列
private final Condition notEmpty;
 
// put的队列
private final Condition notFull;

上記のコードには、takeIndexとputIndexの2つのキーフィールドがあり、それぞれデータを取得してデータを書き込む次回のインデックス位置を示します。したがって、データを追加してデータを取得するときに、計算する必要はありません。追加する場所とデータを取得する場所を知ることができます。

2初期化

初期化中、2つの重要なパラメーターがあります:配列のサイズとそれが適切かどうかソースコードは次のとおりです。

public ArrayBlockingQueue(int capacity, boolean fair) {
    if (capacity <= 0)
        throw new IllegalArgumentException();
    this.items = new Object[capacity];
    lock = new ReentrantLock(fair);
    // 队列不为空 Condition,在 put 成功时使用
    notEmpty = lock.newCondition();
    // 队列不满 Condition,在 take 成功时使用
    notFull =  lock.newCondition();
}

ソースコードから、2番目のパラメーターが公平であるかどうか、主にロックの読み取りと書き込みに使用されるかどうかが確認できます。公平なロックである場合、ロックの競合では、不公平なロックである場合、先着順です。ロックの競合中にランダム。

ロックの公平性と不公平さの例を挙げます。たとえば、キュ​​ーがいっぱいになり、多くのスレッドが書き込み操作を実行しているため、待機をブロックしているスレッドが多くなければなりません。他のスレッドがテイクを実行すると、待機中のスレッドが起動します。公平ロックの場合はブロック待ちの順番でブロックスレッドを起床させ、不公平ロックの場合はランダムにスリープスレッドを起床させます。

したがって、キューがいっぱいで多くのスレッドがput操作を実行する場合、それが公平なロックである場合、配列要素が追加される順序は、ブロッキングスレッドが解放される順序になります。ブロックスレッドが解放される順序はランダムであるため、公平なロックではなく順序があります。 、したがって、要素が配列に挿入される順序は、それらが挿入された順序には従いません。

キューが空の場合も同様です。

ArrayBlockingQueueは、ロックの公平性と不公平性により、配列要素の挿入順序を簡単に実現します。この機能を実現したい場合、どうしますか?ロック機能を使ってみませんか?実際、このアイデアについては記事で何度も触れていますが、1つのことを達成する必要がある場合は、まず既存のAPIが満たされるかどうかを確認します。可能であれば、継承と構成によって実現できますArrayBlockingQueue 就是组合了锁的功能

初期化中に、元のデータが指定されている場合、元のデータのサイズはキューの容量よりも小さい必要があることに注意する必要があります。そうでない場合、次の図に示すように例外がスローされます。
ここに画像の説明を挿入
デモを作成し、エラーは次のとおりです。
ここに画像の説明を挿入

3新しいデータ

putIndexの位置に応じて新しいデータが追加されますソースコードは以下の通りです:

// 新增,如果队列满,无限阻塞
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) {
    // assert lock.getHoldCount() == 1; 同一时刻只能一个线程进行操作此方法
    // assert items[putIndex] == null;
    final Object[] items = this.items;
    // putIndex 为本次插入的位置
    items[putIndex] = x;
    // ++ putIndex 计算下次插入的位置
    // 如果下次插入的位置,正好等于队尾,下次插入就从 0 开始
    if (++putIndex == items.length)
        putIndex = 0;
    count++;
    // 唤醒因为队列空导致的等待线程
    notEmpty.signal();
}

ソースコードから、実際には2つの新しいケースがあることがわかります。

  1. 新しく追加された位置は中央に配置され、直接追加されます。次の図は、putIndexが配列インデックス5の位置にあり、まだキューの最後にないことを示しています。次に、直接追加できます。次の追加位置は6になるはずです。
    ここに画像の説明を挿入
  2. 新しい位置はチームの最後にあるため、次に追加するときは最初から開始します。スケマティックダイアグラムは次のとおりです。
    ここに画像の説明を挿入
    上の図は、このコード行を示しています。if(++ putIndex == items.length)putIndex = 0;

チームの最後に追加すると、次の追加がチームの最初から再開されることがわかります。

4データを取る

データはチームのトップから取得され、ソースコードは次のとおりです。

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        // 如果队列为空,无限等待
        // 直到队列中有数据被 put 后,自己被唤醒
        while (count == 0)
            notEmpty.await();
        // 从队列中拿数据
        return dequeue();
    } finally {
        lock.unlock();
    }
}
 
private E dequeue() {
    final Object[] items = this.items;
    // takeIndex 代表本次拿数据的位置,是上一次拿数据时计算好的
    E x = (E) items[takeIndex];
    // 帮助 gc
    items[takeIndex] = null;
    // ++ takeIndex 计算下次拿数据的位置
    // 如果正好等于队尾的话,下次就从 0 开始拿数据
    if (++takeIndex == items.length)
        takeIndex = 0;
    // 队列实际大小减 1
    count--;
    if (itrs != null)
        itrs.elementDequeued();
    // 唤醒被队列满所阻塞的线程
    notFull.signal();
    return x;
}

ソースコードから、毎回データを取得する位置がtakeIndexの位置であることがわかります。今回取得するデータを見つけた後、takeIndexを1増やして、次にデータを取得するときにインデックスの位置を計算します。特別なケースがあります。今回のデータ取得位置が既にチームの最後尾である場合、次回のデータ取得位置は最初から、つまり0から始まります。

5データを削除する

データの削除は非常に興味深いので、コアソースコードを見てみましょう。

// 一共有两种情况:
// 1:删除位置和 takeIndex 的关系:删除位置和 takeIndex 一样,比如 takeIndex 是 2, 而要删除的位置正好也是 2,那么就把位置 2 的数据置为 null ,并重新计算 takeIndex 为 3。
// 2:找到要删除元素的下一个,计算删除元素和 putIndex 的关系
// 如果下一个元素不是 putIndex,就把下一个元素往前移动一位
// 如果下一个元素是 putIndex,把 putIndex 的值修改成删除的位置
void removeAt(final int removeIndex) {
    final Object[] items = this.items;
    // 情况1 如果删除位置正好等于下次要拿数据的位置
    if (removeIndex == takeIndex) {
        // 下次要拿数据的位置直接置空
        items[takeIndex] = null;
        // 要拿数据的位置往后移动一位
        if (++takeIndex == items.length)
            takeIndex = 0;
        // 当前数组的大小减一
        count--;
        if (itrs != null)
            itrs.elementDequeued();
    // 情况 2
    } else {
        final int putIndex = this.putIndex;
        for (int i = removeIndex;;) {
            // 找到要删除元素的下一个
            int next = i + 1;
            if (next == items.length)
                next = 0;
            // 下一个元素不是 putIndex
            if (next != putIndex) {
                // 下一个元素往前移动一位
                items[i] = items[next];
                i = next;
            // 下一个元素是 putIndex
            } else {
                // 删除元素
                items[i] = null;
                // 下次放元素时,应该从本次删除的元素放
                this.putIndex = i;
                break;
            }
        }
        count--;
        if (itrs != null)
            itrs.removedAt(removeIndex);
    }
    notFull.signal();
}

データを削除する状況はより複雑です。2つのケースがあります。最初のケースはtakeIndex == removeIndexです。処理方法を確認するための概略図を描画しましょう。2
ここに画像の説明を挿入
番目のケースは2つのタイプに分けられます。

  1. removeIndex + 1!= PutIndexの場合、次の要素を1つ前に移動すると、概略図は次のようになります。
    ここに画像の説明を挿入
  2. removeIndex + 1 == putIndexの場合、putIndexの値を削除された位置に変更すると、概略図は次のようになります。
    ここに画像の説明を挿入

ArrayBlockingQueueの削除方法は実際にはかなり複雑であり、多くの特別なシナリオを考慮する必要があります。

6まとめ

ArrayBlockingQueueの下部は境界付き配列です。全体として、他のキューと大差ありません。takeIndexとputIndexがキューの最後に到達すると、それらは再び0から循環し始めることに注意してください。これは特別です。ソースコードを研究するときは特に注意してください。

公開された40元の記事 ウォンの賞賛1 ビュー4983

おすすめ

転載: blog.csdn.net/aha_jasper/article/details/105525628