オーディオとビデオの開発ツアー (55) - ブロッキング キューとロックフリー コンカレント コンテナー

目次

  1. ブロッキング キューの定義と使用シナリオ
  2. ブロッキングキューの実装原理
  3. ConcurrentLinkedQueue とロックフリーの並行コンテナーの CAS を学ぶだけ
  4. 材料
  5. 褒美

1. ブロッキング キューの定義と使用シナリオ

ブロッキング キュー (BlockingQueue) は、Queue Queue に基づいてブロッキングの 2 つのシナリオを追加します

  1. キューがいっぱいになると、キューにデータを追加すると、キューがいっぱいになるまでブロックされます
  2. キューが空の場合、キューからのデータの取得は、キューが空でなくなるまでブロックされます

ブロッキング キューは、生産者と消費者のシナリオでよく使用されます。

最初に Queue および BolckingQueue インターフェースを定義しましょう

//java.util.Queue
public interface Queue<E> extends Collection<E> {

  //添加一个元素到队列,如果队列满时会抛出异常IllegalStateException
  boolean add(E e); 

  //添加一个元素到队列,如果队列满时不会抛异常,而是返回false
  boolean offer(E e);
  
  //从队列中获取并移除一个元素,如果队列为空, 会抛出NoSuchElementException
  E remove();

  //从队列中获取并移除一个元素,如果队列为空, 不会抛异常,而是返回null
  E poll();
  
  //从队列中获取一个元素 但不移除。注意和remove的区别
  //当队列为空时,会抛出异常NoSuchElementException
  E element();
  
  //从队列中获取一个元素,也不移除。注意和poll的区别
  //当队列为空时,不会抛出异常,而是返回null
  E peek();
}

//java.util.concurrent.BlockingQueue
public interface BlockingQueue<E> extends Queue<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;

}

BlockingQueueいくつかのブロッキング メソッドを継承して追加していることがわかりますQueue

Java のインターフェースにはBlockingQueue、次の 7 つの実装クラスがあります。

  1. ArrayBlockingQueue : 追加および取得時に ReentrantLock 再入可能同期ロックを内部的に使用する、配列構造で構成される制限付きブロッキング キュー
  2. LinkedBlockingQueue: リンクされたリスト構造で構成される制限付きブロッキング キュー。追加と取得の際に 2 つの ReentrantLocks が内部的に使用され、スループットは ArrayBlockingQueue よりも高くなります。Executors#newSingleThreadExecutor() と Executors#newFixedThreadPool(int) の両方がこのブロッキング キューを使用します。

  public static ExecutorService newSingleThreadExecutor() {
           return new FinalizableDelegatedExecutorService
               (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));
       }

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
  1. SynchronousQueue: 要素を格納しないブロッキング キュー。各挿入操作は、別のスレッドによって呼び出される削除操作を待機する必要があります。それ以外の場合はブロックされます。通常、スループットは LinkedBlockingQueue よりも高くなります。Executors#newCachedThreadPool() はこのブロッキング キューを使用します

 public static ExecutorService newCachedThreadPool() {
           return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                         60L, TimeUnit.SECONDS,
                                         new SynchronousQueue<Runnable>());
       }
  1. PriorityBlockingQueue: 優先度の並べ替えをサポートする無制限のブロッキング キュー
  2. DelayQueue: 優先キューを使用して実装された要素の遅延取得をサポートする無制限のブロッキング キュー
  3. TransferQueue: リンクされたリスト構造で構成される無制限のブロッキング キュー
  4. BlockingDeque: リンクされたリスト構造で構成される双方向ブロッキング キュー

第二に、ブロックされたキュー (LinkedBlockingQueue) の実装原則

LinkedBlockingQueue を使用して分析します

 //节点结构体 
  static class Node<E> {
        E item;
        Node<E> next;

        Node(E x) { item = x; }
    }


    /** 从队列获取元素时的可重入锁 ,非公平锁*/
    private final ReentrantLock takeLock = new ReentrantLock();

    /** 非空condition,等待队列非空*/
    private final Condition notEmpty = takeLock.newCondition();

    /** 向队列中插入元素时的可重入锁 ,非公平锁*/
    private final ReentrantLock putLock = new ReentrantLock();

    /** 非满condition,等待队列非满 */
    private final Condition notFull = putLock.newCondition();

    /**
     * 当队列有元素后,发出非空信号
     */
    private void signalNotEmpty() {
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
    }

    /**
     * 当队列由满到不满后,发出该非满信号
     */
    private void signalNotFull() {
        final ReentrantLock putLock = this.putLock;
        putLock.lock();
        try {
            notFull.signal();
        } finally {
            putLock.unlock();
        }
    }

2.1 要素をキューに挿入する

オファーの実装 (キューに要素を追加します。キューがいっぱいの場合、例外はスローされませんが、false が返されます)

 public boolean offer(E e) {
        ...
        int c = -1;
        Node<E> node = new Node<E>(e);
        //获取写 可重入锁
        final ReentrantLock putLock = this.putLock;
        putLock.lock();
        try {
            //如果队列还未满,插入该元素节点
            if (count.get() < capacity) {
                // enqueue 插入元素到队列,一会我们在看下其实现
                enqueue(node);
                c = count.getAndIncrement();
                //如果插入后,还队列还未满,发送未满信号
                if (c + 1 < capacity)
                    notFull.signal();
            }
        } finally {
            putLock.unlock();
        }
        // 如果成功插入后,发送非空信号
        if (c == 0)
            signalNotEmpty();
        return c >= 0;
    }

put の実装 (要素をキューに挿入し、キューがいっぱいの場合は空きができるまで待ちます)

public void put(E e) throws InterruptedException {
        ...
        int c = -1;
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
  
        //相比较offer这是差异点1
        //采用了可中断锁,等待过程中可以接收中断
        putLock.lockInterruptibly();
        try {
           //相比较offer这是差异点2,
           //如果当前队列满了,则阻塞,等待非空的信号到来
            while (count.get() == capacity) {
                notFull.await();
            }
            enqueue(node);
            c = count.getAndIncrement();
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
    }

エンキューの実装

 private void enqueue(Node<E> node) {
       //把当前节点作为队列先前未节点的next插入到队列中
       //然后吧last指向新插入的节点
        last = last.next = node;
    }

2.2 キューから要素を取得する

ポーリングの実装 (キューから要素を取得して削除します。キューが空の場合、例外はスローされませんが、null が返されます)

public E poll() {
       ...
        int c = -1;
        //获取取 可重入锁
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
             //如果当前队列的元素个数大于0
            if (count.get() > 0) {
                //dequeue 从队列中获取一个元素,稍后再分析
                x = dequeue();
               //取出后如果队列中元素的个数还大于1
              //(为什么不是大于0? 
              //    这是因为getAndDecrement的实现是先获取再减1),
              // 则发出非空信号
                c = count.getAndDecrement();
                if (c > 1)
                    notEmpty.signal();
            }
        } finally {
            takeLock.unlock();
        }
        //如果c的值等于容器的值(由于getAndDecrement的实现是先获取再减1,这是队列从满变为了非满状态),则发出非满信号
        if (c == capacity)
            signalNotFull();
        return x;
    }

takeの実装(キューの先頭要素を取得、キューが空なら待機)

 public E take() throws InterruptedException {

        ...
        int c = -1;
        final ReentrantLock takeLock = this.takeLock;
        //和poll的差异点1:wait时支持中断
        takeLock.lockInterruptibly();
        try {
          //和poll的差异点2:如果队列为空,则阻塞等待,知道收到非空的信号
            while (count.get() == 0) {
                notEmpty.await();
            }
            x = dequeue();
            c = count.getAndDecrement();
            if (c > 1)
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)
            signalNotFull();
        return x;
    }

デキューの実装

    private E dequeue() {
        //链表操作的通用做法,head是一个虚节点
        Node<E> h = head;
        //头节点的next赋值给定义的first节点
        Node<E> first = h.next;
       //把先前的头节点头的next指向自身节点,方便gc
        h.next = h; // help GC
        //标记新的头节点给到head指针
        head = first;
        //获取元素
        E x = first.item;
        first.item = null;
        return x;
    }

デキューの理解を容易にするために、次のようにリストのノード図を描きます

最初に LinkedBlockingQueue の使用を見て、次にスレッド プールで見てみましょう. 前述のように、Executors#newSingleThreadExecutor() と Executors#newFixedThreadPool(int) の両方が LinkedBlockingQueue を使用します. 「Java Concurrent の技術」から次の 2 つの概略図を見てみましょうプログラミング」ダウン

ArrayBlockingQueue や SynchronousQueue の実装など、他のブロッキング キューの実装はそれ自体で分析できます。

3. ConcurrentLinkedQueue とロックフリーの並行コンテナーの CAS を簡単に学ぶ

上で紹介した LinkedBlockingQueue は、ロックとブロックによってスレッドの安全性を確保します。アルゴリズムのノンブロッキング実装もあります。ConcurrentLinkedQueue は後者を介して実装されています。一緒に分析して学習しましょう。

public class ConcurrentLinkedQueue<E> extends AbstractQueue<E>
        implements Queue<E>, java.io.Serializable {


    private static class Node<E> {
        volatile E item;
        volatile Node<E> next;
    }

    static <E> Node<E> newNode(E item) {
        Node<E> node = new Node<E>();
        //这里的U是sun.misc.Unsafe
        U.putObject(node, ITEM, item);
        return node;
    }

    static <E> boolean casNext(Node<E> node, Node<E> cmp, Node<E> val) {
        return U.compareAndSwapObject(node, NEXT, cmp, val);
    }


public boolean offer(E e) {
        final Node<E> newNode = newNode(Objects.requireNonNull(e));

        for (Node<E> t = tail, p = t;;) {
            Node<E> q = p.next;
            if (q == null) {
                // p is last node
                if (casNext(p, null, newNode)) {
                    if (p != t) // hop two nodes at a time
                        casTail(t, newNode);  // Failure is OK.
                    return true;
                }
                // Lost CAS race to another thread; re-read next
            }
            else if (p == q)
                p = (t != (t = tail)) ? t : head;
            else
                p = (p != t && t != (t = tail)) ? t : q;
        }
    }

}

Unsafe クラス
Unsafe クラスにはメモリを直接操作するメソッドがあります. Java での CAS 操作の実行は Unsafe クラスのメソッドに依存します. Unsafe クラスのすべてのメソッドはネイティブに変更されていることに注意してください. Unsafe クラスが操作を直接呼び出す システムの基礎となるリソースが対応するタスクを実行する

CAS が原子性を保証できるのはなぜですか?
ロックフリー戦略では、CAS と呼ばれる技術を使用してスレッド実行のセキュリティを確保します.
CAS の正式名称は Compare And Swap で、これは比較と交換です. アルゴリズムの核となる考え方は次のとおりです.

CAS(V,E,N)

其包含3个参数
V表示要更新的变量
E表示预期值
N表示新值
//如果V值等于E值,则将V的值设为N。若V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做

CAS 操作を行うスレッドが複数あり、CAS のステップが多いと仮定すると、V と E が同じであると判断してスレッドを切り替え、値が割り当てられようとしているときに値が変更される可能性があります。データの不整合の原因は何ですか?

CAS はオペレーティング システム用語のカテゴリに属する​​システム プリミティブであるため、答えはノーです. いくつかの命令で構成され、特定の機能のプロセスを完了するために使用され、プリミティブの実行は継続的でなければなりません.実行中に中断することは許可されていません。つまり、CAS は CPU のアトミック命令であり、いわゆるデータの不整合の問題は発生しません。

Unsafe のソースコードの解析と理解はまだ少し不十分なので、必要に応じてもう一度見てみましょう. Java 並列処理シリーズはここで終了します.
次に、エンコードとデコードの学習時間を入力し、学習と書き込みのチェックイン グループを確立する準備をします. 興味がある場合は、WeChat「yabin_yangO2O」に私を追加して
ください.一緒に成長します。

4. 情報

  1. 本:「Java並行プログラミングの芸術」
  2. Java 同時ブロッキング キュー LinkedBlockingQueue および ArrayBlockingQueue の詳細な分析
  3. Java 並行プログラミング - ロックフリー CAS および Unsafe クラスとその並行パッケージ Atomic

5.収穫

この記事の学習実践を通して

  1. アプリケーションと Java 同時ブロッキング キューの実装を分析しました。
  2. 単純な分析で CAS とロックフリーの同時実行コンテナーを学習した ConcurrentLinkedQueue

お読みいただきありがとうございます。Java 並行プログラミングはここで終わりを迎え、次の時期はコーディングの学習期に入ります。主に「ビデオコーディングをあらゆる角度から詳しく解説」という書籍を読んで練習する
ためのものです。サイクルとして21日かかります(読み終える必要はありませんが、1日に少なくとも1ページ読んで、少なくとも50語を出力してください)、興味のある友達が一緒に学び、コミュニケーションするために来て、WeChat「yabin_yangO2O」に私を追加してください、ビデオコーディングの読み書きに注意してください

次の記事では、ビデオコーディングの知識の学習と実践を開始します. 公式アカウント「オーディオとビデオの開発の旅」に注目して、一緒に学び、成長してください.

交換へようこそ

おすすめ

転載: blog.csdn.net/u011570979/article/details/119979468