マルチスレッド - キューのブロック

v2-8cffdf8d243386262aee75b587db14bd_b

ブロックキュー

ブロッキング キューもキューです~~ 先入れ先出し。
実際には、必ずしも先入れ先出しに従う必要がない特別なキューがいくつかあります~~ 優先キュー (PriorityQueue)
ブロッキング キューも特別なキューです。ただし、先入れ先出しでもありますが、次のような特別な機能があります。

  1. キューが空の場合、デキュー操作はブロックされ、別のスレッドがキューに要素を追加するまでブロックされます (キューは空ではありません)。

  2. キューがいっぱいの場合、キューの操作もブロックされます。別のスレッドがキューから要素の位置を取得するまでブロックされます (キューはいっぱいではありません)。

メッセージ キューも特別なキューです。指定されたカテゴリに従って先入れ先出しを実行するには、ブロッキング キューに基づいて「メッセージ タイプ」を追加するのと同じです。今回話しているメッセージ キューは次のとおりです。まだ「データ構造」です。

このメッセージ キューは非常に使いやすいため、一部の専門家はこのデータ構造を別のプログラムに実装し、このプログラムはネットワークを介して他のプログラムと通信できます。

現時点では、このメッセージ キューをサーバーのグループに個別にデプロイ (分散) することができ、ストレージ容量と転送容量が大幅に向上しており、このようなメッセージ キューは多くの大規模プロジェクトで見られます

現在、メッセージキューはMySQLやRedisに匹敵する重要なコンポーネント(ミドルウェア)となっています。
メッセージキューの代表的な実装としてはrabbit mqがあります。他にも実装はあります。active mq、rocket mq、kafka...などはどれも良いです。 - 業界で既知のメッセージ キュー。

メッセージキューを明確に理解したい場合は、まず「ブロッキングキュー」について理解する必要があります。

メッセージ キューはなぜ使いやすいのでしょうか? それは、ブロッキング キューのブロッキング特性と大きく関係しています!!!
このような特性に基づいて、「プロデューサー/コンシューマー モデル」を実装できます。

中国の旧正月には、大晦日の夕食と餃子作りも含まれます~~ 家族全員が一緒に餃子を作ります~~ 一部の地域では、旧正月にもちむすびを食べます
。皮巻き+餃子作り

2 つの典型的な梱包方法:

  1. みんなで餃子の皮を伸ばす+餃子を別々に作る
  2. 餃子の皮を伸ばすのは一人が担当 餃子の皮を伸ばすたびにカーテンの上に置き、もう一人が包装を担当 カーテンから餃子の皮を取り出して包む ⇒ この方法を生産者消費といいます. またはモデル。

生産者消費者モデル

プロデューサー/コンシューマー パターンは、コンテナーを介してプロデューサーとコンシューマーの間の強い結合の問題を解決します。
プロデューサーとコンシューマーは相互に直接通信するのではなく、ブロッキング キューを介して通信します。したがって、プロデューサーがデータを生成した後、コンシューマーがデータを処理するのを
待つプロデューサにデータを要求しませんが、データはブロッキング キューから直接取得されます。

このとき、餃子の皮を広げる責任者は生産者、
餃子を作る責任者は消費者で、
カーテンを掛けると行列が妨げられます。

餃子の皮を伸ばす人の巻き方が遅すぎると、餃子を作る人が待たされてしまいますし、餃子の皮を伸ばす人の巻き方が
速すぎると、餃子を作る人が包みきれなくなります。すぐに餃子の皮を広げる人が止まってしまいますので、降りてしばらくお待ちください。

画像-20231002091352683


生産者/消費者モデルは、私たちのプログラムに 2 つの非常に重要な利点をもたらします。

1.送信側と受信側の「デカップリング」を実現

~~ 結合を減らすプロセスを「デカップリング」と呼びます。
結合された 2 つのモジュール間の相関が強いか弱いか
~~ 相関が強いほど、結合は高くなります。

開発における一般的なシナリオ: サーバー間の相互呼び出し。

画像-20231002095819632

画像-20231002110305649

2.「山を削り、谷を埋める」ことを実現し、システムの安定性を確保します。

画像-20231002111335375

三峡ダムの効果は~~頂上を切り崩して埋め立てることだ

雨量がピークに達し、上流の水量が増えると洪水が発生し、ダムが水を堰き止めることで下流への流れは比較的緩やかになり、乾季には三峡ダムが開いて放水される

画像-20231002120015605

サーバー開発は上記のモデルと非常に似ており、
アップストリームはユーザーによって送信されたリクエストであり、
ダウンストリームは特定のサービスを実行するいくつかのサーバーです。

ユーザーはどれくらいのリクエストを送信しますか? それは制御できません。リクエストが多い場合もあれば、リクエストが少ない場合もあります...
このとき、ホット検索 (Weibo ホット検索など) の概念が登場します。
そのワードがホット検索になります 社会現象に関連したものです 例えばコロナ禍では「流行」…がホット検索ワードになります もしかしたら、ある瞬間に多くのユーザーからリクエストが来るかもしれませ
んあなたのために~~ サーバーは、各リクエストを処理するためにハードウェア リソースを消費する必要があります。(CPU、メモリ、ハードディスク、帯域幅など) を含むがこれらに限定されず、特定のハードウェア リソースがボトルネックに達すると、サーバーはハングアップします。これはシステムの安定性にリスクをもたらします!!! 生産者消費者モデルを使用することは
効果的な方法です!!!

標準ライブラリが提供するブロッキングキューを使用する

public interface BlockingDeque<E> extends BlockingQueue<E>, Deque<E> {}

LinkedBlockingQueue<> (java.util.concurrent)=> リンクリストに基づいて実装されたブロッキングキュー
priorityBlockingQueue<> (java.util.concurrent)=> 優先度付きのブロッキングキュー~~ データ構造ヒープに基づいて実装
ArrayBlockingQueue<> (java.utii.concurrent)=> 配列に基づいて実装されたブロッキングキュー

Queue には 3 つのメソッドが用意されています。

  1. キュー~~オファー
  2. デキュー~~ポーリング
  3. チームの最初の要素を取得~~ 覗いてみましょう

キューをブロックするには主に 2 つの方法があります。

  1. キュー(ブロック機能付き) ~~ put
  2. デキュー (ブロック機能付き) ~~ take
public static void main(String[] args) throws InterruptedException {
    
    
    BlockingDeque<String> blockingDeque = new LinkedBlockingDeque<>();
    blockingDeque.put("fly0213"); // 入队列
    String res = blockingDeque.take(); // 出队列. 
    System.out.println(res);
    res = blockingDeque.take(); // 队列中没有元素了, take, 就会阻塞.
    System.out.println(res);
}

次に、標準ライブラリのブロッキング キューに基づいて、単純なプロデューサー/コンシューマー モデル コードを作成します。

import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;

/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: fly(逐梦者)
 * Date: 2023-10-02
 * Time: 14:30
 */
public class ThreadDemo22 {
    
    
    public static void main(String[] args) {
    
    
        BlockingDeque<Integer> blockingDeque = new LinkedBlockingDeque<>();

        // 创建两个线程, 来作为生产者和消费者
        Thread customer = new Thread(() -> {
    
    
            while (true) {
    
    
                try {
    
    
                    Integer result = blockingDeque.take();
                    System.out.println("消费元素: " + result);
                } catch (InterruptedException e) {
    
    
                    throw new RuntimeException(e);
                }
            }
        });
        customer.start();

        Thread producer = new Thread(() -> {
    
    
            int count = 0;
            while(true){
    
    
                try {
    
    
                    blockingDeque.put(count);
                    System.out.println("生产元素: "+ count);
                    count++;
                    Thread.sleep(500);
                } catch (InterruptedException e) {
    
    
                    throw new RuntimeException(e);
                }
            }
        });
        producer.start();
    }
}

簡単なブロッキングキューを自分で実装する

~~ ブロッキング キュー コードを作成することで、マルチスレッドについて理解を深めることができます

ブロッキング キューを実装するには、次の 3 つの手順が必要です。

  1. まず、通常のキューを作成します。キューの実装は、配列またはリンク リストに基づいて行うことができます (先頭の削除/末尾の挿入を実装するのが簡単です)。
  2. さらにスレッドの安全性も向上します。
  3. ブロック機能を追加します。

注: リンク リストに基づいて通常のキューを実装する場合、先頭の削除と末尾の挿入の計算量は O(1) ですが、リンク リストの先頭の削除の計算量は元々 O(1) であるため、料金を支払う必要はありません
。注意してください、これはリンク リストの末尾削除操作にすぎません。O(1) の時間計算量を達成するには、追加の参照を使用して現在の終了ノードを記録する必要があります~~ 関連するコードは難しくなく、ブロガーは簡単に実行できます説明する!

配列を使用して列をループし、ブロッキング キューを実装します。
画像-20231002174113723

1. 通常のキューを実装するコード

/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: fly(逐梦者)
 * Date: 2023-10-02
 * Time: 15:08
 */

// 自己写的阻塞队列
// 注: 此处不考虑泛型, 直接使用 int 来表示元素类型
class MyBlockingQueue {
    
    
    private int[] items = new int[1000];
    private int head = 0;
    private int tail = 0;
    private int size = 0;

    // 入队列
    public void put(int value) {
    
    
        if (size == items.length) {
    
    
            // 队列满了, 不能继续插入
            return;
        }
        items[tail] = value;
        tail++;
        // 对 tail 进行处理
        // 第一种写法
        // tail = tail % items.length;

        // 第二种写法 ( 更推荐, 可读性更高 & % 在效率上没有优势 )
        if (tail >= items.length) {
    
    
            tail = 0;
        }
        size++;
    }

    // 出队列
    public Integer take() {
    
    
        if (size == 0) {
    
    
            // 队列空, 不能出队列
            return null;
        }
        int result = items[head];
        head++;
        if (head >= items.length) {
    
    
            head = 0;
        }
        size--;
        return result;
    }
}

public class ThreadDemo23 {
    
    
    public static void main(String[] args) {
    
    
        MyBlockingQueue queue = new MyBlockingQueue();
        queue.put(1);
        queue.put(2);
        queue.put(3);
        queue.put(4);
        int result = 0;
        result = queue.take();
        System.out.println("result = " + result);
        result = queue.take();
        System.out.println("result = " + result);
        result = queue.take();
        System.out.println("result = " + result);
        result = queue.take();
        System.out.println("result = " + result);
    }
}

2. ブロッキング関数~~ は、キューをマルチスレッド環境で使用する必要があることを意味します

スレッドの安全性を確保するために、主なことはロックを追加することです~~ メソッド内で同期されたパッケージput()take()すべてのコードを使用します もちろん、メソッドに同期を追加することも可能です。

// 自己写的阻塞队列
// 注: 此处不考虑泛型, 直接使用 int 来表示元素类型
class MyBlockingQueue {
    
    
    private int[] items = new int[1000];
    private int head = 0;
    private int tail = 0;
    private int size = 0;

    // 入队列
    public void put(int value) {
    
    
        synchronized (this) {
    
    
            if (size == items.length) {
    
    
                // 队列满了, 此时要产生阻塞
                // return;
                try {
    
    
                    this.wait();
                } catch (InterruptedException e) {
    
    
                    throw new RuntimeException(e);
                }
            }
            items[tail] = value;
            tail++;
            // 对 tail 进行处理
            // 第一种写法
            // tail = tail % items.length;

            // 第二种写法 ( 更推荐, 可读性更高 & % 在效率上没有优势 )
            if (tail >= items.length) {
    
    
                tail = 0;
            }
            size++;

            // 这个 notify 唤醒 take 中的 wait
            this.notify();
        }
    }

    // 出队列
    public Integer take() {
    
    
        int result = 0;
        synchronized (this) {
    
    
            if (size == 0) {
    
    
                // 队列空, 此时也需要阻塞
                try {
    
    
                    this.wait();
                } catch (InterruptedException e) {
    
    
                    throw new RuntimeException(e);
                }
                return null;
            }
            result = items[head];
            head++;
            if (head >= items.length) {
    
    
                head = 0;
            }
            size--;
            // 唤醒 put 中的 wait
            this.notify();
        }
        return result;
    }
}

画像-20231002175701861

これら 2 つのスレッドの待機が同時にトリガーされる可能性はありますか?
~~ 同時にトリガーされた場合、明らかに相互に正しくウェイクアップできません
。答えはノーです。同じキューの場合、それはできません。満腹でも空っぽでもない限り、シュレーディンガーの猫みたいに、ふふふ!

3. 上記のコードには別の欠陥があります。

if (size == items.length) { this.wait(); }

wait がウェイクアップされるとき、この時点で if の条件が true であってはなりません?
具体的には、wait in put がウェイクアップされた後、キューがフルであってはなりませんが、wait
ウェイクアップされた後もキューがフルであってはなりません。
ただし、この状況は発生しません。現在のコードは、要素が正常にフェッチされた後にのみ起動する必要があります。要素がフェッチされるたびに起動します。ただし、安全のために、最善の方法は、再度判断することです。待機
が戻った後、こちらを参照してください その時間の条件は満たされていますか?
例: 朝起きて授業に行くときと同様に、目覚まし時計を 8 時 20 分にセットする必要があります。鳴る、起きる時間です。
でも、7時に起きれば、授業までにはまだ早いことが分かり(まだ条件が整っていません)、まだ少し寝ていられます(目が覚めるたびに) 、起きる条件が満たされているかどうかを判断するために時間を確認する必要があります)。

if (size == items.length) {
    
    
    this.wait();      
    if (size == items.length) {
    
    
        this.wait();
     } 
}

このように書くのは二度目の判断ですが、不適切です、もしかしたら待ちが二度目に目覚めて、まだ条件が満たされていないのかもしれません~~

4. コードの完全版


/**
 * Created with IntelliJ IDEA.
 * Description:
 * User: fly(逐梦者)
 * Date: 2023-10-02
 * Time: 15:08
 */

// 自己写的阻塞队列
// 注: 此处不考虑泛型, 直接使用 int 来表示元素类型
class MyBlockingQueue {
    
    
    private int[] items = new int[1000];
    private int head = 0;
    private int tail = 0;
    private int size = 0;

    // 入队列
    public void put(int value) {
    
    
        synchronized (this) {
    
    
            while (size == items.length) {
    
     // 标准库就建议这么写!!!
                // 队列满了, 此时要产生阻塞
                // return;
                try {
    
    
                    this.wait();
                } catch (InterruptedException e) {
    
    
                    throw new RuntimeException(e);
                }
            }
            items[tail] = value;
            tail++;
            // 对 tail 进行处理
            // 第一种写法
            // tail = tail % items.length;

            // 第二种写法 ( 更推荐, 可读性更高 & % 在效率上没有优势 )
            if (tail >= items.length) {
    
    
                tail = 0;
            }
            size++;

            // 这个 notify 唤醒 take 中的 wait
            this.notify();
        }
    }

    // 出队列
    public Integer take() {
    
    
        int result = 0;
        synchronized (this) {
    
    
            while (size == 0) {
    
    
                // 队列空, 此时也需要阻塞
                try {
    
    
                    this.wait();
                } catch (InterruptedException e) {
    
    
                    throw new RuntimeException(e);
                }
            }
            result = items[head];
            head++;
            if (head >= items.length) {
    
    
                head = 0;
            }
            size--;
            // 唤醒 put 中的 wait
            this.notify();
        }
        return result;
    }
}

public class ThreadDemo23 {
    
    
    public static void main(String[] args) {
    
    
        MyBlockingQueue queue = new MyBlockingQueue();
        queue.put(1);
        queue.put(2);
        queue.put(3);
        queue.put(4);
        int result = 0;
        result = queue.take();
        System.out.println("result = " + result);
        result = queue.take();
        System.out.println("result = " + result);
        result = queue.take();
        System.out.println("result = " + result);
        result = queue.take();
        System.out.println("result = " + result);
    }
}

おすすめ

転載: blog.csdn.net/m0_73740682/article/details/133499098