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

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

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

入門用の言葉
キューに関して言えば、私はこれを使ったことがないというのが皆さんの反応かもしれません。そう思うと、それは大きな間違いです。通常、スレッドプール、読み取り/書き込みロック、メッセージキュー、およびその他のテクノロジとフレームワークを使用します。基本的な原則はキューであるため、キューを過小評価しないでください。キューは多くの高度なものですAPIの基礎とキューの学習は、Javaを深く学ぶために非常に重要です。

この記事では主に、LinkedBlockingQueueキューを例として、最下層の特定の実装を詳細に説明します。

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

LinkedBlockingQueue中国語は、リンクリストブロックキューと呼ばれます。この名前は非常に適切です。名前から、基になるデータ構造はリンクリストであり、キューはブロック可能であることがわかります。次に、構造全体からLinkedBlockingQueueを確認します。

1.1クラス図

まず、LinkedBlockingQueueクラス図を次のように見てみましょう。クラス図
ここに画像の説明を挿入
から、おそらく2つのパスを確認できます。

  1. AbstractQueue-> AbstractCollection-> Collection-> Iterableこのパス依存関係は、主にコレクションとイテレータの一部の操作を再利用するためのものです。コレクションについて話すとき、これらのクラスの機能と実行可能な機能はすべてわかっているため、詳しく説明しません。以上
  2. BlockingQueue-> Queue-> Collection、BlockingQueueおよびQueueは、2つの新しいインターフェースです。

キューは最も基本的なインターフェースです。ほとんどすべてのキュー実装クラスがこのインターフェースを実装します。このインターフェースは、キューに対して次の3種類の操作を定義します:
新しい操作:

  1. 追加キューがいっぱいになると、例外がスローされます。
  2. オファーキューがいっぱいの場合はfalse。
    操作の表示と削除:
  3. 削除キューが空の場合、例外がスローされます。
  4. ポーリングキューが空の場合、nullを返します。
    削除せずに表示のみ:
  5. 要素キューが空の場合、例外がスローされます。
  6. ピークキューが空の場合、nullが返されます。

上記の分類方法に加えて、合計6つの方法も2つのカテゴリに分類できます。

  1. キューがいっぱいまたは空の場合、追加、削除、要素などの例外をスローします。
  2. キューがいっぱいまたは空の場合、offer、poll、peekなどの特別な値を返します。

実際、これらを覚えるのはより困難です。このメソッドを使用する必要があるたびに、ソースコードを調べて、このメソッドが例外をスローするか、特別な値を返すかを思い出します。

BlockingQueueは、常にブロックする、一定期間ブロックするなど、キューに基づいてブロックするという概念を追加します。メモリを容易にするために、次のようにテーブルを描画します。

運営 例外を投げる 特別な価値 ブロックを続ける しばらくブロックする
新しい操作キューがいっぱいです 追加 オファーはfalseを返します プット タイムアウト後にオファーがfalseを返す
空の操作キューの表示と削除 削除する 投票はnullを返します 取る タイムアウト後にポーリングがnullを返す
操作のみを表示し、削除はしません-キューは空です 素子 ピークはnullを返します いいえ いいえ

PS:BlockingQueueクラスのアノテーションで定義されているremoveメソッドは例外をスローしますが、LinkedBlockingQueueのremoveメソッドは実際にはfalseを返します。

表からわかるように、BlockingQueueは、追加、表示、削除という2つの主要な操作でブロックを追加し、常にブロックするか、一定期間ブロックした後で特別な値に戻すかを選択できます。

1.2クラスのメモ

LinkedBlockingQueueのクラスアノテーションから取得できる情報を見てみましょう。

  1. リンクリストのブロッキングキューに基づいて、基になるデータ構造はリンクリストです。
  2. リンクされたリストは先入れ先出しのキューを維持し、新しい要素はチームの最後に配置され、取得された要素はチームのヘッドから取得されます。
  3. リンクリストのサイズは初期化中に設定でき、デフォルトはIntegerの最大値です。
  4. 2つのインターフェースが実装されているため、CollectionとIteratorの2つのインターフェースのすべての操作を使用できます。

1.3内部構成

LinkedBlockingQueueの内部構造は、単純に3つの部分に分かれてい链表存储 + 锁 + 迭代器ます。ソースコードを見てみましょう。

// 链表结构 begin
//链表的元素
static class Node<E> {
    E item;
 
    //当前元素的下一个,为空表示当前节点是最后一个
    Node<E> next;
 
    Node(E x) { item = x; }
}
 
//链表的容量,默认 Integer.MAX_VALUE
private final int capacity;
 
//链表已有元素大小,使用 AtomicInteger,所以是线程安全的
private final AtomicInteger count = new AtomicInteger();
 
//链表头
transient Node<E> head;
 
//链表尾
private transient Node<E> last;
// 链表结构 end
 
// 锁 begin
//take 时的锁
private final ReentrantLock takeLock = new ReentrantLock();
 
// take 的条件队列,condition 可以简单理解为基于 ASQ 同步机制建立的条件队列
private final Condition notEmpty = takeLock.newCondition();
 
// put 时的锁,设计两把锁的目的,主要为了 take 和 put 可以同时进行
private final ReentrantLock putLock = new ReentrantLock();
 
// put 的条件队列
private final Condition notFull = putLock.newCondition();
// 锁 end
 
// 迭代器 
// 实现了自己的迭代器
private class Itr implements Iterator<E> {
………………
}

コードの観点から見ると、構造は非常に明確であり、3つの構造が機能します。

  1. リンクリストの役割は、現在のノードを保存することです。ノードのデータは何でもかまいません。これは一般的なタイプです。たとえば、キュ​​ーがスレッドプールに適用されると、ノードはスレッドになります。ノードの意味は、主にキューが使用されるシーンに依存します。
  2. キュー操作中のスレッドの安全性を確保するために、テイクロックとプットロックがありますが、テイクとプット操作が互いに影響を与えることなく同時に実行できるように、2種類のロックが設計されています。

1.4初期化

初期化には3つの方法があります。

  1. リンクリストのサイズを指定します。
  2. リンクリストのサイズを指定しないでください。デフォルトはIntegerの最大値です。
  3. 既存の収集データを初期化します。

ソースコードは次のとおりです。

// 不指定容量,默认 Integer 的最大值
public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE);
}
// 指定链表容量大小,链表头尾相等,节点值(item)都是 null
public LinkedBlockingQueue(int capacity) {
    if (capacity <= 0) throw new IllegalArgumentException();
    this.capacity = capacity;
    last = head = new Node<E>(null);
}
 
// 已有集合数据进行初始化
public LinkedBlockingQueue(Collection<? extends E> c) {
    this(Integer.MAX_VALUE);
    final ReentrantLock putLock = this.putLock;
    putLock.lock(); // Never contended, but necessary for visibility
    try {
        int n = 0;
        for (E e : c) {
            // 集合内的元素不能为空
            if (e == null)
                throw new NullPointerException();
            // capacity 代表链表的大小,在这里是 Integer 的最大值
            // 如果集合类的大小大于 Integer 的最大值,就会报错
            // 其实这个判断完全可以放在 for 循环外面,这样可以减少 Integer 的最大值次循环(最坏情况)
            if (n == capacity)
                throw new IllegalStateException("Queue full");
            enqueue(new Node<E>(e));
            ++n;
        }
        count.set(n);
    } finally {
        putLock.unlock();
    }
}

初期化ソースコードについて、2つのポイントを説明します:

  1. 初期化中、容量のサイズはパフォーマンスに影響を与えず、後の使用にのみ影響します。初期化キューが小さすぎるため、キューがいっぱいであるというエラーが発生しやすく、その量は報告されません。
  2. 特定のデータセットを初期化すると、ソースコードは洗練されていないデモンストレーションを提供します。現在のリンクリストのサイズが各forループの容量を超えているかどうかをチェックすることは反対ではありませんが、forループが始まる前に開始したいと考えています。この種の作業の1つのステップを実行します。たとえば、指定されたセットのサイズは1 wであり、リンクリストのサイズは9kです。現在のコード実装によると、forループが9k回の場合にのみ検出されます。指定されたセットの元のサイズはすでにリンクリストのサイズよりも大きいため、9kサイクルになります。これはリソースの浪費です。forループの前に1回確認することをお勧めします。1w> 9kの場合は、エラーを報告してください。

2新しいブロッキング

追加、配置、提供など、多くの新しい方法があります。3つの違いは上記のとおりです。putメソッドを例にとると、putメソッドは、キューがいっぱいになるまでブロックし、キューがいっぱいでなくなるまで実行され続けます。ソースコードは次のとおりです。

// 把e新增到队列的尾部。
// 如果有可以新增的空间的话,直接新增成功,否则当前线程陷入等待
public void put(E e) throws InterruptedException {
    // e 为空,抛出异常
    if (e == null) throw new NullPointerException();
    // 预先设置 c 为 -1,约定负数为新增失败
    int c = -1;
    Node<E> node = new Node<E>(e);
    final ReentrantLock putLock = this.putLock;
    final AtomicInteger count = this.count;
    // 设置可中断锁
    putLock.lockInterruptibly();
    try {
        // 队列满了
        // 当前线程阻塞,等待其他线程的唤醒(其他线程 take 成功后就会唤醒此处被阻塞的线程)
        while (count.get() == capacity) {
            // await 无限等待
            notFull.await();
        }
 
        // 队列没有满,直接新增到队列的尾部
        enqueue(node);
 
        // 新增计数赋值,注意这里 getAndIncrement 返回的是旧值
        // 这里的 c 是比真实的 count 小 1 的
        c = count.getAndIncrement();
 
        // 如果链表现在的大小 小于链表的容量,说明队列未满
        // 可以尝试唤醒一个 put 的等待线程
        if (c + 1 < capacity)
            notFull.signal();
 
    } finally {
        // 释放锁
        putLock.unlock();
    }
    // c==0,代表队列里面有一个元素
    // 会尝试唤醒一个take的等待线程
    if (c == 0)
        signalNotEmpty();
}
// 入队,把新元素放到队尾
private void enqueue(Node<E> node) {
    last = last.next = node;
}

ソースコードから、次の点を要約できます。

  1. キューにデータを追加するには、最初のステップはロックすることです。そのため、新しいデータはスレッドセーフです。
  2. キュー内の新しいデータは、リンクリストの最後に追加するだけです。
  3. 追加すると、キューがいっぱいになると、現在のスレッドがブロックされます。ブロックの最下層はロックする機能です。最下層の実装もキューに関連しています。ロックの章で説明する原則
  4. 新しいデータが正常に追加された後、適切なタイミングで、プットの待機スレッド(キューがいっぱいでない場合)またはテイクの待機スレッド(キューが空でない場合)がウェイクアップされ、プットまたはテイク条件が満たされたときにキューをすぐにウェイクアップできるようにします。スレッドをブロックして実行を継続し、呼び起こす時間が無駄にならないようにします。

上記はputメソッドの原理です。offerメソッドの複数回のブロックについては、依然として失敗し、デフォルト値の実装に直接戻ります。putメソッドと比較すると、次のスクリーンショットに示すように、数行のコードのみが変更されています。
ここに画像の説明を挿入

3ブロック削除

削除する方法はたくさんありますが、主に2つの重要な問題を取り上げます。

  1. 削除の原則は何ですか。
  2. 表示と削除、表示のみと削除ではない違いを理解する方法。

最初に、最初の問題を見てみましょう。takeメソッドを例にして、表示および削除される基になるソースコードについて説明します。

// 阻塞拿数据
public E take() throws InterruptedException {
    E x;
    // 默认负数,代表失败
    int c = -1;
    // count 代表当前链表数据的真实大小
    final AtomicInteger count = this.count;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lockInterruptibly();
    try {
        // 空队列时,阻塞,等待其他线程唤醒
        while (count.get() == 0) {
            notEmpty.await();
        }
        // 非空队列,从队列的头部拿一个出来
        x = dequeue();
        // 减一计算,注意 getAndDecrement 返回的值是旧值
        // c 比真实的 count 大1
        c = count.getAndDecrement();
        
        // 如果队列里面有值,从 take 的等待线程里面唤醒一个。
        // 意思是队列里面有值啦,唤醒之前被阻塞的线程
        if (c > 1)
            notEmpty.signal();
    } finally {
        // 释放锁
        takeLock.unlock();
    }
    // 如果队列空闲还剩下一个,尝试从 put 的等待线程中唤醒一个
    if (c == capacity)
        signalNotFull();
    return x;
}
// 队头中取数据
private E dequeue() {
    Node<E> h = head;
    Node<E> first = h.next;
    h.next = h; // help GC
    head = first;
    E x = first.item;
    first.item = null;// 头节点指向 null,删除
    return x;
}

全体的なプロセスは、最初にロックしてからキューの先頭からデータを取得するputと非常によく似ています。キューが空の場合、キューに値が設定されるまでブロックされます。

ビューは、要素を削除しない、より簡単に、あなたは、キューデータの頭出しに直接来ることができる、次のように、我々は、例えば、ソースコードを覗い:

// 查看并不删除元素,如果队列为空,返回 null
public E peek() {
    // count 代表队列实际大小,队列为空,直接返回 null
    if (count.get() == 0)
        return null;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        // 拿到队列头
        Node<E> first = head.next;
        // 判断队列头是否为空,并返回
        if (first == null)
            return null;
        else
            return first.item;
    } finally {
        takeLock.unlock();
    }
}

チームヘッドからデータを取得するロジックは、表示と削除の間で一貫しておらず、削除ではないことがわかります。これにより、チームヘッドのデータが削除され、削除されません。

4まとめ

この記事では、LinkedBlockingQueueのソースコードを通じてリンクリストキューを紹介します。キューがいっぱいで空の場合、データを追加および削除すると、キューはどうなりますか?

キュー自体はブロッキングツールです。このツールをさまざまなブロッキングシナリオに適用できます。たとえば、キュ​​ーはスレッドプールに適用されます。スレッドプールがなくなると、すべての新しいリクエストをブロッキングキューに入れて待機します。キューアプリケーションメッセージキューでは、コンシューマーの処理能力が制限されている場合、メッセージをキューに入れて、コンシューマーがゆっくりと消費するのを待つことができます。新しいシナリオに適用されるたびに、それは新しい技術ツールであるため、キューを学習します。便利です。

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

おすすめ

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