LinkedBlockingQueue Source Parsing

Posted by virtuexru on Fri, 17 May 2019 07:15:43 +0200

In the last blog, we introduced Array BlockQueue, which is a bounded blocking queue based on arrays. Now that there are arrays, there must be linked list-based queues. Yes, of course, there are. This is our protagonist today: Linked BlockingQueue. Array BlockQueue is bounded, so is Linked BlockingQueue bounded or unbounded? I think it can be said to be bounded or unbounded. Why do you say that? Look down and you'll see.

Like the previous blog, let's first look at the basic application of LinkedBlockingQueue, and then parse the core code of LinkedBlockingQueue.

Basic Application of LinkedBlockingQueue

    public static void main(String[] args) throws InterruptedException {
        LinkedBlockingQueue<Integer> linkedBlockingQueue = new LinkedBlockingQueue();

        linkedBlockingQueue.add(15);
        linkedBlockingQueue.add(60);
        linkedBlockingQueue.offer(50);
        linkedBlockingQueue.put(100);

        System.out.println(linkedBlockingQueue);

        System.out.println(linkedBlockingQueue.size());

        System.out.println(linkedBlockingQueue.take());
        System.out.println(linkedBlockingQueue);

        System.out.println(linkedBlockingQueue.poll());
        System.out.println(linkedBlockingQueue);

        System.out.println(linkedBlockingQueue.peek());
        System.out.println(linkedBlockingQueue);

        System.out.println(linkedBlockingQueue.remove(50));
        System.out.println(linkedBlockingQueue);
    }

Operation results:

[15, 60, 50, 100]
4
15
[60, 50, 100]
60
[50, 100]
50
[50, 100]
true
[100]

The code is relatively simple. First try to analyze it.

  1. A Linked Blocking Queue was created.
  2. The add/offer/put method is used to add elements to LinkedBlockingQueue, where the add method is executed twice.
  3. Print out LinkedBlockingQueue: [15, 60, 50, 100].
  4. Print out the size: 4 of LinkedBlockingQueue.
  5. Use the take method to pop up the first element and print it out: 15.
  6. Print out LinkedBlockingQueue: [60, 50, 100].
  7. Use the poll method to pop up the first element and print it out: 60.
  8. Print out LinkedBlockingQueue: [50, 100].
  9. Use the peek method to pop up the first element and print it out: 50.
  10. Print out LinkedBlockingQueue: [50, 100].
  11. Using the remove method, remove the element with a value of 50 and return true.
  12. Print out LinkedBlockingQueue: 100.

The code is relatively simple, but there are still some details that are not clear:

  • How does the underlying layer ensure thread security?
  • Where is the data stored and in what form?
  • offer/add/put is all about adding elements to the queue. What's the difference?
  • Pol/take/peek are all elements of the pop-up queue. What's the difference?

To solve the above questions, the best way is to look at the source code. Let's look at the core source code of LinkedBlockingQueue.

LinkedBlockingQueue Source Parsing

Construction method

LinkedBlockingQueue provides three constructive methods, as shown in the following figure:

Let's analyze it one by one.

LinkedBlockingQueue()
    public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }

The non-parametric construction method throws out the "pot" directly and gives another construction method, but we should pay attention to the passing parameters: Integer.MAX_VALUE.

LinkedBlockingQueue(int capacity)
    public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        last = head = new Node<E>(null);
    }
  1. Judge whether the incoming capacity is legal, if not greater than 0, throw an exception directly.
  2. Assign the incoming capacity to capacity.
  3. Create a new Node node and assign it to the head and last fields.

What is this capacity? If you have a sense of code, it should be easy to guess that this is the maximum capacity of LinkedBlockingQueue. If we call the parametric construction method to create LinkedBlockingQueue, its maximum capacity is Integer.MAX_VALUE, which we call "unbounded", but we can also specify the maximum capacity, then this queue is a "bounded" queue, so some blogs are very hasty to say that LinkedBlockingQueue is a bounded queue, or an unbounded queue. People think this is not rigorous.

Let's see what this Node is.

    static class Node<E> {
        E item;

        Node<E> next;

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

Is there an inexplicable sense of intimacy? Obviously, this is the realization of one-way linked list. Next points to the next Node.

LinkedBlockingQueue(Collection<? extends E> c)
    public LinkedBlockingQueue(Collection<? extends E> c) {
        this(Integer.MAX_VALUE);//Calling the second constructor, the incoming capacity is the maximum value of Int, which can be said to be an unbounded queue.
        final ReentrantLock putLock = this.putLock;
        putLock.lock(); //Open exclusive lock
        try {
            int n = 0;//The size used to record LinkedBlockingQueue
            //Loop-in c-SET
            for (E e : c) {
                if (e == null)//If e==null, a null pointer exception is thrown
                    throw new NullPointerException();
                if (n == capacity)//If n==capacity indicates maximum capacity, a "Queue full" exception is thrown
                    throw new IllegalStateException("Queue full");
                enqueue(new Node<E>(e));//Team entry operation
                ++n;//n self increment
            }
            count.set(n);//Set count
        } finally {
            putLock.unlock();//Release Exclusive Locks
        }
    }
  1. Calling the second constructor passes in the maximum value of int, so you can say that LinkedBlockingQueue is an unbounded queue at this point.
  2. Open the exclusive lock putLock.
  3. A variable n is defined to record the size of the current LinkedBlockingQueue.
  4. If the element is null, the null pointer exception is thrown. If n==capacity indicates the maximum capacity, the "Queue full" exception is thrown. Otherwise, enqueue operation is executed to enter the queue, and N increases itself.
  5. Setting count to n shows that count is the size of LinkedBlockingQueue.
  6. Release the exclusive lock putLock in final.

offer

    public boolean offer(E e) {
        if (e == null) throw new NullPointerException();//If the input element is NULL, throw an exception
        final AtomicInteger count = this.count;//Remove count
        if (count.get() == capacity)//If count==capacity, which indicates maximum capacity, return false directly.
            return false;
        int c = -1;//Representing size
        Node<E> node = new Node<E>(e);//New Node Node
        final ReentrantLock putLock = this.putLock;
        putLock.lock();//Open exclusive lock
        try {
            if (count.get() < capacity) {//If count < capacity, the maximum capacity has not been reached.
                enqueue(node);//Team entry operation
                c = count.getAndIncrement();//Get count and assign it to c to complete the self-increment operation
                if (c + 1 < capacity)//If C + 1 < capacity, there is still room to wake up a thread blocked by calling the await method of notFull
                    notFull.signal();
            }
        } finally {
            putLock.unlock();//Release exclusive locks in final
        }
        if (c == 0)//If c==0 indicates that when putLock is released, there is an element in the queue, signalNotEmpty is called
            signalNotEmpty();
        return c >= 0;
    }
  1. If the element passed in is null, an exception is thrown.
  2. The count of this class instance is assigned to the local variable count.
  3. If count==capacity, which indicates the maximum capacity, return false directly.
  4. Define a local variable c to denote size with an initial value of -1.
  5. New Node node.
  6. Open the exclusive lock putLock.
  7. If count >= capacity, indicating the maximum capacity, after releasing the exclusive lock, return false, because at this time c=-1, C >= 0 is false; if count < capacity, indicating that there is still room, continue to execute. Here we need to think about a question, why the third step has judged whether there is still room, and here we have to judge again? Because there may be multiple threads executing the add/offer/put method, when the queue is not full, multiple threads simultaneously execute the third step (when the third step has not opened the exclusive lock), and then go down at the same time, so when the exclusive lock is opened, it needs to be re-judged.
  8. Perform the enrollment operation.
  9. After getting count and assigning it to c, the self-incremental operation is completed. Note that the first assignment and then self-increment, assignment and self-increment order will directly affect the subsequent judgment logic.
  10. If c + 1 < capacity, there is still room to wake up a thread blocked by calling the await method of notFull. Why do we have to make a + 1 judgment here? Because in step 9, the size of LinkedBlockingQueue is first assigned and then increased, that is to say, the local variable c is saved or the size of LinkedBlockingQueue before joining the team, so the size of LinkedBlockingQueue is obtained by + 1 operation.
  11. In final, release the exclusive lock putLock.
  12. If c==0, the signalNotEmpty method is called if there is and only one element in the queue when the putLock exclusive lock is released. Let's look at signalNotEmpty method:
    private void signalNotEmpty() {
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
    }

The code is relatively simple, which is to open an exclusive lock and wake up a thread blocked by calling the await method of notEmpty, but it is important to note that the exclusive lock obtained here is no longer putLock, but takeLock.

add

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

The add method calls the offer method directly, but the add method is not exactly the same as the offer method. When the queue is full, if the offer method is called, it will return false directly, but if the add method is called, an exception of "Queue full" will be thrown.

put

    public void put(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();//If the input element is NULL, throw an exception
        int c = -1;//Representing size
        Node<E> node = new Node<E>(e);//New Node Node
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;//Get count
        putLock.lockInterruptibly();//Open exclusive lock
        try {
            //If the maximum capacity is reached, call the await method of notFull, wait for wake-up, and use the while loop to prevent false wake-up.
            while (count.get() == capacity) {
                notFull.await();
            }
            enqueue(node);//Join the team
            c = count.getAndIncrement();//Counts are assigned to c first, and then self-incremental
            if (c + 1 < capacity)//If c+1<capacity, call the signal method of notFull to wake up a thread blocked by calling the await method of notFull
                notFull.signal();
        } finally {
            putLock.unlock();//Release Exclusive Locks
        }
        if (c == 0)//If there is an element in the queue, wake up a thread blocked by calling the await method of notEmpty
            signalNotEmpty();
    }
  1. If the input element is NULL, an exception is thrown.
  2. Define a local variable c to denote size with an initial value of -1.
  3. New Node node.
  4. Assign count in instances of this class to the local variable count.
  5. Open the exclusive lock putLock.
  6. If the maximum capacity is reached, call the await method of notFull, block the current thread, and wait for other threads to call the signal method of notFull to wake themselves up. Here, use the while loop to prevent false wake-up.
  7. Perform the enrollment operation.
  8. count is assigned to c first, and then increases itself.
  9. If c+1<capacity indicates that there is still room, call the signal method of notFull to wake up the thread blocked by calling the await method of notFull.
  10. Release exclusive lock putLock.
  11. If there is only one element in the queue, wake up the thread blocked by calling the await method of notEmpty.

enqueue

    private void enqueue(Node<E> node) {
        last = last.next = node;
    }

Is the entry operation particularly simple, that is, to assign the incoming Node node to the next field of the last node, and then to the last field, so as to form a one-way list?

Small summary

So far, the core source code of offer/add/put has been analyzed. Let's make a brief summary. offer/add/put are all methods of adding elements, but there are differences between them. When the queue is full, calling the above three methods will lead to different situations:

  • offer: Return to false directly.
  • add: Although the offer method is also called internally, the queue is full and an exception is thrown.
  • put: Threads will block and wait to wake up.

size

    public int size() {
        return count.get();
    }

There's nothing to say. count records the size of LinkedBlockingQueue and returns.

take

    public E take() throws InterruptedException {
        E x;
        int c = -1;//size
        final AtomicInteger count = this.count;//Get count
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lockInterruptibly();//Open exclusive lock
        try {
            while (count.get() == 0) {//Explain that there is no data in the current queue
                notEmpty.await();//Blocking, Waiting to Wake Up
            }
            x = dequeue();//Team out
            c = count.getAndDecrement();//Assignment first, then subtraction
            if (c > 1)//If size > 1, there are at least two elements in the queue before leaving the queue
                notEmpty.signal();//Wake up a thread blocked by calling the await method of notEmpty
        } finally {
            takeLock.unlock();//Release Exclusive Locks
        }
        if (c == capacity)//If there is still a space left in the queue
            signalNotFull();
        return x;
    }
  1. Define a local variable c to denote size with an initial value of -1.
  2. Assign the count field of this class instance to the temporary variable count.
  3. Open the exclusive lock takeLock that responds to interruptions.
  4. If count==0 indicates that there is no data in the current queue, the current thread will be blocked and awaited until other threads call the signal method of notEmpty to wake up the current thread. The purpose of using the while loop is to prevent false awakening.
  5. Conduct queue operation.
  6. count assigns value to c first, then decreases itself. Here we need to pay attention to assigning value first, then decreases itself.
  7. If C > 1, that is, size > 1, combined with the above assignment, and then decreases, we know that if the condition is satisfied, it means that there are at least two elements in the queue before leaving the queue, we call the signal method of notEmpty to wake up the thread blocked by calling the await method of notEmpty.
  8. Release the exclusive lock takeLock.
  9. If there is only one remaining space in the queue after the queue is executed, in other words, the signalNotFull method is called if the queue is full before the queue is executed.

Let's look at signalNotFull method again:

    private void signalNotFull() {
        final ReentrantLock putLock = this.putLock;
        putLock.lock();
        try {
            notFull.signal();
        } finally {
            putLock.unlock();
        }
    }
  1. Open the exclusive lock. Note that the exclusive lock here is putLock.
  2. Call the signal method of notFull to wake up a thread blocked by calling the await method of notFull.
  3. Release exclusive lock putLock.

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;
        takeLock.lock();
        try {
            if (count.get() > 0) {
                x = dequeue();
                c = count.getAndDecrement();
                if (c > 1)
                    notEmpty.signal();
            }
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)
            signalNotFull();
        return x;
    }

Compared with the take method, the biggest difference is that if the queue is empty, executing the take method blocks the current thread until it is waked up, and the poll method returns null directly.

peek

    public E peek() {
        if (count.get() == 0)
            return null;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            Node<E> first = head.next;
            if (first == null)
                return null;
            else
                return first.item;
        } finally {
            takeLock.unlock();
        }
    }

The peek method only takes the value of the header node, but it does not remove the node.

dequeue

   private E dequeue() {
        Node<E> h = head;
        Node<E> first = h.next;
        h.next = h; // help GC
        head = first;
        E x = first.item;
        first.item = null;
        return x;
    }

There's nothing to say, just pop-up elements, and remove pop-up elements.

Small summary

So far, the core source code of take/poll/peek has been analyzed. Let's make a brief summary. Take/poll/peek are all methods to get the value of the header node, but there are still some differences between them:

  • take: When the queue is empty, the current thread is blocked until it is awakened. Out-of-queue operations are performed to remove the acquired nodes.
  • poll: When the queue is empty, return null directly. Out-of-queue operations are performed to remove the acquired nodes.
  • put: When the queue is empty, return null directly. Nodes will not be removed.

LinkedBlockingQueue's core source code analysis is done here. Thank you.

Topics: Java