BlockingQueue for Concurrent Programming (II)

Preface:

The first article in our Pickup series has parsed the first and second layers of the three-tier structure in the concurrent package, and detailed code analysis of the lock in the third layer. This blog article will do source code analysis for BlockingQueue, the basic data structure that will be used in the subsequent executor, in preparation for the subsequent Executor source code analysis.

Let's first look at the definition of Doug Lea. BlockingQueue is a queue that supports two special operations: the threads that get elements when the queue is empty need to wait, and the threads that add elements when the queue is full need to wait. This is a typical application of producer-consumer model.

Top-level interfaces are set up in three ways:

Three methods of BlockingQueue


Because put()/take() methods can block in concurrency, we focus on these two methods.

Famous family members of BlockingQueue:

Array BlockingQueue, LinkedBlockingQueue, SynchronousQueue, we use these three classes as the basis for source code analysis of put (), take () methods.

1: ArrayBlockingQueue:

Let's first look at several important member variables of this class.

    final Object[] items; //Array based

    int takeIndex;        //Next to get the location of the element

    int putIndex;        //The next place to place the element

    int count;            //Number of elements in the queue

    final ReentrantLock lock;    //Locks, thread exclusive when used for put and take

    private final Condition notEmpty;    //Condition s wait queues where the queued threads add elements to the waiting queue

    private final Condition notFull;        //Condition waits for queues in which the queued threads are waiting for queue element consumption

The producer-consumer model is mainly embodied in two queues, notEmpty and notFull, the former is the consumer queue, the latter is the producer queue, just like the restaurant waiter (consumer) and cook (producer) in the window, the former waits for the dishes in the window, and the latter waits for the window to have the place to put the dishes.

The producer method put:

public void put(E e) throws InterruptedException {

        checkNotNull(e);

        final ReentrantLock lock = this.lock;          //Getting locks, the same locks used by producers and consumers, you can't produce when consumers consume.

        lock.lockInterruptibly();                               //Closure, the exclusive monopoly of current producers

        try {

            while (count == items.length)                //Allow current producers to wait for slots in the producer queue when the queue is full

                notFull.await();

            enqueue(e);                                           //Adding elements to the queue when the queue is not full is thread exclusive.

        } finally {

            lock.unlock();

        }

    }

private void enqueue(E x) {

        final Object[] items = this.items;        

        items[putIndex] = x;                                //Place the x element in the slot of the array where the element can be placed

        if (++putIndex == items.length)               //Adding an operation to the placement of elements

            putIndex = 0;                                       //If there is no place to place the element, set the position of the element to 0.

        count++;                                                  //Number of elements + 1

        notEmpty.signal();                                    //Notify the consumer queue that there is something to consume now

    }

The above method can be summarized as follows: there are many cooks in the kitchen. When a cook has finished the dish, he will put the dish in the window exclusively. At this time, other cooks need to wait in line for the cook to finish the dish. At this time, if the window is full, the cook will wait in the producer queue of the window for the waiter to take the dish away from the window. If a team of programmers are waiting in the consumer queue during the process of the chef occupying the window, the chef will notify the consumer queue to pick up the dishes after putting the dishes in the window.

Consumer approach take:

public E take() throws InterruptedException {

        final ReentrantLock lock = this.lock;      //The lock here is the same lock as the producer's lock, so you can't consume it when the producer produces it.

        lock.lockInterruptibly();

        try {

            while (count == 0)                            //If the queue is empty, wait for the queue to become non-empty

                notEmpty.await();                        //Waiting queue becomes non-empty

            return dequeue();                            //If there are elements in the queue, the method to fetch them is called

        } finally {

            lock.unlock();

        }

    }

private E dequeue() {

        final Object[] items = this.items;

        E x = (E) items[takeIndex];                           //Getting an element is exactly the opposite of putting it in.

        items[takeIndex] = null;

        if (++takeIndex == items.length)

            takeIndex = 0;

        count--;

        if (itrs != null)

            itrs.elementDequeued();

        notFull.signal();                              //Notify the producer queue that there are no elements here. You need to produce them.

        return x;

    }

OK! Code analysis shows that the bottom layer of Array BlockingQueue is Condition s and Lock s, which we analyzed in the last article. The reason is very simple, but have you found that blocking queues can not be produced and consumed simultaneously? This is why BlockingQueue is not widely used in third-party frameworks.

2: LinkedBlockingQueue:

Several member variables of this class:

private final int capacity;          //capacity

AtomicInteger count = new AtomicInteger();    //Number of queue elements

private transient Node last;                                //Tail pointer

private final ReentrantLock takeLock = new ReentrantLock();  //Locks occupied by call methods such as take and poll

private final Condition notEmpty = takeLock.newCondition();    //Consumer waiting queue, waiting queue becomes non-empty

private final ReentrantLock putLock = new ReentrantLock();     //Locks occupied by calls such as put, offer, etc.

private final Condition notFull = putLock.newCondition();         //Producer waiting queue

Producer put method:

public void put(E e) throws InterruptedException {

        if (e == null)

            throw new NullPointerException(); 

        int c = -1;

        Node node = new Node(e);                            //Create a new node based on the element to be placed

        final ReentrantLock putLock = this.putLock;   //Getting Producer Locks

        final AtomicInteger count = this.count;            

        putLock.lockInterruptibly();                                //The current producer has a monopoly on the code

        try {

            while (count.get() == capacity) {                    //The same routine, if the current queue is full, wait in the producer's Condition queue for signal

                notFull.await();

            }

            enqueue(node);                                              //If it's not full, enter the queue directly. There's no need for cas because only the current thread calls this method.

            c = count.getAndIncrement();                        //Number of queue elements + 1

            if (c + 1 < capacity)                                        //If the queue is not full after placing the element, notify other producer threads that it can be produced here.

                notFull.signal();

        } finally {

            putLock.unlock();                                           

        }

        if (c == 0)

            signalNotEmpty();                                            //Finally, c==0 proves that there are elements in the queue, so inform the consumer to come and spend.

    }

Consumer take method:

public E take() throws InterruptedException {

        E x;

        int c = -1;

        final AtomicInteger count = this.count;

        final ReentrantLock takeLock = this.takeLock;        

        takeLock.lockInterruptibly();                //Exclusive consumer locks, where there is only one thread when an element is retrieved

        try {

            while (count.get() == 0) {                    //If the queue is empty, then wait in the Condition consumer waiting queue

                notEmpty.await();

            }

            x = dequeue();                                            //If the queue is not empty, then take the elements calmly, don't be afraid of being robbed by others, because you are the only one.

            c = count.getAndDecrement();                

            if (c > 1)

                notEmpty.signal();                                   //If there are more than two elements in the queue, notify the consumer to wait for the other consumers in the queue to pick them up.

        } finally {

            takeLock.unlock();

        }

        if (c == capacity)                                            //If c==capacity indicates that there is a vacancy in the queue, the producer queue thread production is notified at this time.

            signalNotFull();

        return x;

    }

Linked Blocking Queue has the advantage of using two locks compared with Array Blocking Queue, which ensures that one producer and one consumer work simultaneously. This is an improvement, but not multiple producers and multiple consumers work simultaneously.

3: SynchronousQueue class:

To be honest, this class is not very clear yet, but later Executors source code is useful, so it is necessary to parse it. These two days are very busy, so let's put them on hold for the time being.

Posted by wreed on Tue, 14 May 2019 20:55:55 -0700