プロデューサーの消費者モデルは、Javaで話す--BlockingQueue

序文

プロデューサ/コンシューマモデル私はあなたが慣れていないと確信していますが、非常に一般的な分散型リソーススケジューリングモデルです。生産者と消費者:このモデルでは、少なくとも2つのオブジェクトがあります。資源の唯一の責任ある利用の資源、消費者を作成するための唯一の責任プロデューサー。彼らは、単純なプロデューサ/コンシューマモデルを達成した場合行うためのキューよりも何でも非常に簡単ですが、このアプローチは、多くの隠れた欠陥を持っています:

  1. スレッドの視認性を確保するために必要なリソースは、同時に手動でスレッドの同期を実装します
  2. 私たちは、危機的な状況および拒否ポリシーのさまざまなを考慮する必要があります
  3. スループットとスレッドセーフの間のバランスを維持する必要性

だから、Javaは簡単な分析のLinkedBlockingQueueを使用し、我々は、インタフェースとその実装クラス用のBlockingQueueのだろう、我々の前にインタフェースと実装の良いパッケージされています

ブロッキングキュー

コンセプト

BlockingQueueのは、ブロックキューを意味する、私たちはクラス定義から見ることができ、それはキューインターフェイスを継承し、それをキューとして使用することができます。

図1

今ブロッキングキューと呼ばれ、そのキューは、次の2つの面で具体、この操作方法をブロックしています。

  • 挿入された要素は、動作がブロックされている:キューがいっぱいになったとき、糸挿入操作がブロック行われます
  • 操作ブロッキング要素を取り外す場合:スレッドをキューが空である場合、実行除去動作が阻止されます

これにより、容易に生産者と消費者との関係を調整することができます

インタフェースメソッド

BlockingQueueのでは、以下の6つのインタフェースが定義されています。

public interface BlockingQueue<E> extends Queue<E> {
    boolean add(E e);

    boolean offer(E e);

    void put(E e) throws InterruptedException;

    boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException;

    E take() throws InterruptedException;

    E poll(long timeout, TimeUnit unit) throws InterruptedException;

    int remainingCapacity();

    boolean remove(Object o);

    public boolean contains(Object o);

    int drainTo(Collection<? super E> c);

    int drainTo(Collection<? super E> c, int maxElements);
}
复制代码

機能に係るインターフェース方式は、次の3つのカテゴリに分けることができます。

  • 、追加のオファー、置く:要素が含ま追加
  • 要素を削除し、次のとおり、削除ポーリング、取る、drainTo
  • 取得/チェック要素が含まれます:、remainingCapacityが含まれています

一般的に、我々は、とも呼ばれる要素を追加しますput(使用した場合であっても操作offer方法は、代わりputの要素を除去、メソッドと呼ばれる)をtake操作します

最初の二つのカテゴリーでは、例外処理は、次のカテゴリに再び道をたどることができます。

  • 例外をスロー:削除し、追加します
  • 特別な値を返します:提供(e)は、世論調査
  • ブロッキング:置く(e)は、撮ります
  • タイムアウト終了:プラン(E、時間、単位)、ポーリング(時間、単位)

治療これらのタイプの私は文字通りの意味があったことは明らかである、説明しません。

ブロッキングキューの実現

JDK8は、次のBlockQueue実装クラスを提供します。

図2

私たちは、次のような基本的な使用しました:

  • ArrayBlockingQueue:ArrayListの実装に基づくブロッキングキュー、有界
  • LinkedBlockingQueue:ブロックキューを実装するのLinkedListに基づいて、有界
  • PriorityBlockingQueue:優先度キュー、無制限
  • DelayQueue:サポート遅延要素が優先度キューを取得し、無制限

残りの関心が自己理解を得ることができ、我々は、例えばここでLinkedBlockingQueueにあるJavaがキューをブロック達成することである方法を説明します

インタフェースメソッド

提供されるインタフェースメソッドBlockingQueueのに加えて、LinkedBlockingQueueはまた、方法を提供するpeek最初のチームのノードを取得するために

この時点で、我々はブロッキングキュー方法を要約する表で、ここで終わっ説明した使用、[1]

方法/アプローチ 例外を投げます 特別な値を返します。 おもり タイムアウト終了
要素を挿入 (e)を追加 プラン(E) 置く(E) プラン(E、タイムアウト、単位)
要素を削除します 削除() 世論調査() 取る() 世論調査(タイムアウト、単位)
要素を取得します 素子() ピーク() / /

前記element方法とpeek機能的に同一の方法

プロパティ

BlockingQueueのが唯一のインタフェース仕様を定義して、真の実現は、特定のカテゴリで行われ、私たちはいくつかの重要なドメインオブジェクトLinkedBlockingQueueを検索するには直接、中央のAbstractQueueをスキップしてみましょう:

    /** 元素个数 */
    private final AtomicInteger count = new AtomicInteger();
    
    /** 队首节点 */
    transient Node<E> head;
    /** 队尾节点 */
    private transient Node<E> last;

    /** take、poll等方法持有的锁,这里叫做take锁或出锁 */
    private final ReentrantLock takeLock = new ReentrantLock();
    /** take方法的等待队列 */
    private final Condition notEmpty = takeLock.newCondition();

    /** put、offer等方法持有的锁,这里叫做put锁或入锁  */
    private final ReentrantLock putLock = new ReentrantLock();
    /** put方法的等待队列 */
    private final Condition notFull = putLock.newCondition();
复制代码

ノードノードは、通常のキューノードであり、LinkedListのは、我々は後ろに2つのカテゴリに分けることができる4つのドメインのオブジェクトに焦点を当てる:挿入要素のため、及び要素を除去します。ここで、各クラスには2つの属性があります。ReentranLockConditionこれReentranLockAQSに基づいている[2]リエントラントロック実装(リエントラント概念を理解していない通常のロックとして使用することができる)、Conditionそれはより強力を提供する方法を理解できるように待機/通知特定のインプリメンテーションモード(waitそしてnotifyクラス)

count自然言うまでもなくのプロパティと言って、headそしてlast私は彼らが手の込んだしていないと信じているキューの記憶素子を維持するために使用されることは明らかです。キューと通常の差別ポイントをブロックすると、キューの後ろということですReentrantLockし、Condition次の数のモジュールでこれらの4つのプロパティの意味について4つのプロパティのタイプは、詳細な分析を行います

我々は説明次の次を容易にするために、のは、簡単に説明させてCondition、このカテゴリを。実際には、Conditionインタフェース、AQSで特定のカテゴリ。この記事では、あなただけの3つのメソッドを知っておく必要がありますawait()signal()singalAll()これら3つの方法は、完全に類似することができwait()notify()そしてnotifyAll()それらの間の差がぼやけ、と理解することができるwait/notify管理する方法であるオブジェクトのロックとロックのタイプを、それらがスレッドのロック操作キューを待っているとawait/signal、これらのプロセスが管理されていますAQSベースのロック自然AQSスレッドの待機キュー操作

だからここだnotEmpty待機維持するためにtake锁、スレッドキューをnotFull待ち維持put锁スレッドキューを。また良く、文字通りの意味で理解notEmpty「キューが空でない」を意味し、その要素は同じトークンを取ることができ、notFullそれが「ない完全なキュー」を意味し、あなたの中の要素を挿入することができます

要素を挿入

プラン(E)

初めて目offer(e)の方法は、以下のソースコード:

    public boolean offer(E e) {
        if (e == null) throw new NullPointerException();
        final AtomicInteger count = this.count;
        // 如果容量达到上限会返回false
        if (count.get() == capacity)
            return false;
        int c = -1;
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        // 获取put锁
        putLock.lock();
        try {
            if (count.get() < capacity) {
                // 入队并自增元素个数
                enqueue(node);
                // 注意,这里c返回的是增加前的值
                c = count.getAndIncrement();
                // 如果容量没到上限,就唤醒一个put操作
                if (c + 1 < capacity)
                    notFull.signal();
            }
        } finally {
            // 解锁
            putLock.unlock();
        }
        if (c == 0)
            // 如果队列之前为空,会唤醒一个take操作
            signalNotEmpty();
        return c >= 0;
    }
复制代码

この方法は、ほとんどの操作がうまくあなたが操作の要素が許可されていない追加するとき、理解されているoffer方法は、ユーザーが返されますfalseコミュニケーションを非ブロックする方法と同様に、。offerスレッドセーフなアプローチが通じているput锁ことを確認するために、

非常に興味深いところは、私たちが最後の判断を見れば、そこにあるc == 0、それは目を覚ますだろうtake運転を。私たちはここに裁判官を増やす必要があり、なぜ多くの人々は、それがプロセスを通じて、ということで、不思議に思うかもしれませんc初期値がされ-1、その値が唯一の場所である修正c = count.getAndIncrement()この文。それが決定された場合は、他の言葉では、c == 0、この文の戻り値がある0要素を挿入する前に、キューが空であること。開始キューが空である場合したがって、最初の要素が挿入されたとき、すぐに目を覚ますであろうtake動作を[3]

次のようにこの時点で、全体の処理の流れをまとめることができます。

  1. 得ますput锁
  2. チーム内の要素、およびインクリメントcount
  3. 容量が上限、ウェイクアップ達していない場合はput、操作を
  4. キューが空の場合、挿入前の最後の要素でのウェークアップtake操作

プラン(E、タイムアウト、単位)

私たちは、その後にメカニズムを見て進捗状況を構築するoffer方法:

    public boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        long nanos = unit.toNanos(timeout);
        int c = -1;
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        // 可被中断地获取put锁
        putLock.lockInterruptibly();
        try {
            // 重复执行while循环体,直到队列不满,或到了超时时间
            while (count.get() == capacity) {
                // 到了超时时间后就返回false
                if (nanos <= 0)
                    return false;
                // 会将当前线程添加到notFull等待队列中,
                // 返回的是剩余可用的等待时间
                nanos = notFull.awaitNanos(nanos);
            }
            enqueue(new Node<E>(e));
            c = count.getAndIncrement();
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
        return true;
    }
复制代码

そして、全体のプロセスは、実質的にoffer(e)同じ手順、二つの異なる点があります:

  1. ロックを取得することは割り込みの形、すなわちを使用していますputLock.lockInterruptibly()
  2. キューが常に満杯である場合、それはループが実行されますnotFull.awaitNanos(nanos)するために、現在のスレッドを追加するための操作をnotFullキューで待機し(待つput操作が実行されるように)

そして、残りのoffer(e)と全く同じで、ここでは詳細には触れていません

(e)を追加

add方法及びoffer方法に比べ、操作が許可されていない場合、次のように、例外は、特別な値を返すのではなく、スローされます。

    public boolean add(E e) {
        if (offer(e))
            return true;
        else
            throw new IllegalStateException("Queue full");
    }
复制代码

単純にすることではありませんoffer(e)、第二のパッケージを行うと言うことは何も、あなたがこのメソッドの実装を言及する必要があることが重要であるということですAbstractQueue

置く(E)

put(e)操作方法は、スレッドをブロックするために許可されるとき、私たちはそれを見て実現する方法です。

    public void put(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        int c = -1;
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        
        // 以可中断的形式获取put锁
        putLock.lockInterruptibly();
        try {
            // 与offer(e, timeout, unit)相比,采用了无限等待的方式
            while (count.get() == capacity) {
                // 当执行了移除元素操作后,会通过signal操作来唤醒notFull队列中的一个线程
                notFull.await();
            }
            enqueue(node);
            c = count.getAndIncrement();
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
    }
复制代码

方法が類似している間案の定、put(e)我々が話す前に、操作を比較することができoffer(e, timeout, unit)、唯一の別の場所にキューが一杯になったとき、それは、ありませんawait、すなわち、操作はもはや時間切れであるだけ待つことができtake、操作[4]起動するsignal方法をスレッドを覚まします

要素を削除します

世論調査()

poll()最初のチームノードに除去し、復帰するための方法は、以下の方法の具体的な実装であります:

    public E poll() {
        final AtomicInteger count = this.count;
        if (count.get() == 0)
            return null;
        E x = null;
        int c = -1;
        final ReentrantLock takeLock = this.takeLock;
        // 获取take锁
        takeLock.lock();
        try {
            if (count.get() > 0) {
                // 出队,并自减
                x = dequeue();
                c = count.getAndDecrement();
                if (c > 1)
                    // 只要队列还有元素,就唤醒一个take操作
                    notEmpty.signal();
            }
        } finally {
            takeLock.unlock();
        }
        // 如果在队列满的情况下移除一个元素,会唤醒一个put操作
        if (c == capacity)
            signalNotFull();
        return x;
    }
复制代码

あなたが慎重に読めばoffer(e)、以下の方法を、poll()方法は話すことは何もない、完全であるoffer(e)レプリカが(私も何かを言いたいが、poll()方法が完全であるとoffer(e)...プロセスとまったく同じ)

他の

poll(timeout, unit)/take()/remove()方法があるoffer(e, timeout, unit)/put()/add()特別な場所がありません、レプリカ法、ここでの合計はスキップ

要素を取得します

ピーク()

peek()この方法は、次のように実装された最初の要素にチームを取得するために使用されます。

    public E peek() {
        if (count.get() == 0)
            return null;
        final ReentrantLock takeLock = this.takeLock;
        // 获取take锁
        takeLock.lock();
        try {
            Node<E> first = head.next;
            if (first == null)
                return null;
            else
                return first.item;
        } finally {
            takeLock.unlock();
        }
    }
复制代码

、この方法は、取得する必要があることに留意すべきであると言うことは何も処理しないtake锁、それはで言うことですpeek()実行方法の時間、の要素を削除する操作を実行することができません

素子()

element()メソッドの実装はですAbstractQueue

    public E element() {
        E x = peek();
        if (x != null)
            return x;
        else
            throw new NoSuchElementException();
    }
复制代码

あるいは、同じ二次包装作業

概要

これは、それはBlockingQueue、結果が長時間言いますLinkedBlockingQueueしかし、古典ブロッキングキュー実装として、LinkedBlockingQueueアイデアの実現の方法はまたのためのブロッキングキューを理解するために非常に重要です。ブロッキングキューの概念を理解したい、最も重要なことは、次のような、ロックの概念を理解することですLinkedBlockingQueue通じ生产者锁/put锁および消费者锁/take锁、ならびに対応するロックConditionスレッドセーフな目的を達成します。私は、全体を理解するためには、この点を理解して生产者/消费者模型


  1. ここで参照は、「Javaの並行プログラミングのアート」に作られて↩︎

  2. 参照してくださいAQS(抽象キューシンクロナイザ)で記事↩︎

  3. ここで、「ウェイクアップとして説明したtake」待機ウェイクアップ動作が「やや不正確で、実際のように記述しなければならないtake锁、スレッドを」私は前者が理解するための詳細なヘルプの読者だと思うので、記述するために、元の方法に従う↩︎を

  4. それが指すtake含む、同様の方法の官能基をtake/poll/removeput同じように動作します↩︎

おすすめ

転載: juejin.im/post/5cf71ffb51882524156c9b8d