Audio and video development tour (55) - blocking queue and lock-free concurrent container

Table of contents

  1. Definition and usage scenarios of blocking queue
  2. Implementation principle of blocking queue
  3. Simply learn ConcurrentLinkedQueue and CAS of lock-free concurrent containers
  4. material
  5. reward

1. Definition and usage scenarios of blocking queue

Blocking Queue (BlockingQueue) adds two scenarios of blocking on the basis of Queue Queue

  1. When the queue is full, adding data to the queue will block until the queue is full
  2. When the queue is empty, getting data from the queue will block until the queue becomes non-empty

Blocking queues are often used in producer-consumer scenarios

Let's first define the Queue and BolckingQueue interfaces

//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;

}

We can see BlockingQueuethat it inherits from Queueand adds several blocking methods.

There are seven implementation classes for interfaces in Java BlockingQueue, which are as follows:

  1. ArrayBlockingQueue : A bounded blocking queue composed of an array structure, internally using a ReentrantLock reentrant synchronization lock when adding and acquiring
  2. LinkedBlockingQueue: A bounded blocking queue composed of a linked list structure. Two ReentrantLocks are used internally when adding and getting, and the throughput is higher than ArrayBlockingQueue. Both Executors#newSingleThreadExecutor() and Executors#newFixedThreadPool(int) use this blocking queue

  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: A blocking queue that does not store elements. Each insert operation must wait for a remove operation called by another thread, otherwise it is blocked. Throughput is generally higher than LinkedBlockingQueue. Executors#newCachedThreadPool() uses this blocking queue

 public static ExecutorService newCachedThreadPool() {
           return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                         60L, TimeUnit.SECONDS,
                                         new SynchronousQueue<Runnable>());
       }
  1. PriorityBlockingQueue: an unbounded blocking queue that supports priority sorting
  2. DelayQueue: An unbounded blocking queue that supports delayed acquisition of elements implemented using a priority queue
  3. TransferQueue: an unbounded blocking queue composed of a linked list structure
  4. BlockingDeque: a two-way blocking queue composed of a linked list structure

Second, the implementation principle of the blocked queue (LinkedBlockingQueue)

We use LinkedBlockingQueue to analyze

 //节点结构体 
  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 Inserting elements into the queue

Implementation of offer (add an element to the queue, if the queue is full, no exception will be thrown, but false will be returned)

 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;
    }

Implementation of put (insert an element into the queue, if the queue is full, wait until there is space available)

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();
    }

Implementation of enqueue

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

2.2 Get elements from the queue

Implementation of poll (get and remove an element from the queue, if the queue is empty, no exception will be thrown, but null will be returned)

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;
    }

Implementation of take (get the head element of the queue, if the queue is empty, wait)

 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;
    }

Implementation of dequeue

    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;
    }

In order to facilitate the understanding of dequeue, draw the node diagram of the list as follows

Let's look at the use of LinkedBlockingQueue first and then in the thread pool. As mentioned earlier, Executors#newSingleThreadExecutor() and Executors#newFixedThreadPool(int) both use LinkedBlockingQueue. Let's look at the following two schematic diagrams from "The Art of Java Concurrent Programming" Down

The implementation of other blocking queues can be analyzed by itself, such as the implementation of ArrayBlockingQueue and SynchronousQueue.

3. Simply learn ConcurrentLinkedQueue and CAS of lock-free concurrent containers

The LinkedBlockingQueue introduced above ensures thread safety by locking and blocking. There is also a non-blocking implementation of the algorithm. ConcurrentLinkedQueue is implemented through the latter, let's analyze and study together.

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 class
There are methods to directly operate memory in the Unsafe class. The execution of CAS operations in Java depends on the methods of the Unsafe class. Note that all methods in the Unsafe class are natively modified, that is to say, the methods in the Unsafe class directly call the operation The underlying resources of the system perform corresponding tasks

Why can CAS guarantee atomicity?
The lock-free strategy uses a technology called CAS to ensure the security of thread execution. The
full name of CAS is Compare And Swap, which is comparison and exchange. The core idea of ​​​​the algorithm is as follows

CAS(V,E,N)

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

Assuming that there are multiple threads performing CAS operations and there are many steps in CAS, is it possible to switch threads and change the value after judging that V and E are the same and just about to assign a value. What caused the data inconsistency?

The answer is no, because CAS is a system primitive, which belongs to the category of operating system terms. It is composed of several instructions and is used to complete a process of a certain function, and the execution of the primitive must be continuous. It is not allowed to be interrupted during execution, which means that CAS is an atomic instruction of the CPU , and will not cause the so-called data inconsistency problem.

The analysis and understanding of the source code of Unsafe is still somewhat insufficient, so let’s look at it again as needed. The Java concurrency series has come to an end here.
Next, enter the learning time of encoding and decoding, and prepare to establish a learning and writing check-in group. If you are interested, welcome to add me on WeChat "yabin_yangO2O", note video encoding, reading and writing
, and learn and grow together.

4. Information

  1. Book: "The Art of Java Concurrent Programming"
  2. In-depth analysis of java concurrent blocking queue LinkedBlockingQueue and ArrayBlockingQueue
  3. Java concurrent programming - lock-free CAS and Unsafe class and its concurrent package Atomic

5. Harvest

Through the learning practice of this article

  1. Analyzed the application and implementation of java concurrent blocking queue
  2. Simple analysis learned CAS and lock-free concurrent container ConcurrentLinkedQueue

Thank you for reading, Java concurrent programming has come to an end here, and the next period of time will enter the learning time of coding.
It is mainly for the reading and practice of the book "Detailed Explanation of Video Coding from All Angles" . Take 21 days as a cycle (you don’t have to finish reading, but read at least one page a day and output at least 50 words), interested friends can come to learn and communicate together, add me on WeChat "yabin_yangO2O", note video coding reading and writing

In the next article, we will start the learning and practice of video coding knowledge. Welcome to pay attention to the official account "Audio and Video Development Journey" and learn and grow together.

Welcome to exchange

Guess you like

Origin blog.csdn.net/u011570979/article/details/119979468