Java source code analysis and interview questions-LinkedBlockingQueue source code analysis

This series of related blog, Mu class reference column Java source code and system manufacturers interviewer succinctly Zhenti
below this column is GitHub address:
Source resolved: https://github.com/luanqiu/java8
article Demo: HTTPS: // GitHub. com / luanqiu / java8_demo
classmates can look at it if necessary)

Java source code analysis and interview questions-LinkedBlockingQueue source code analysis

Introductory words
When it comes to queues, everyone's reaction may be that I have never used it. It should be an unimportant API. If you think so, it is a big mistake. We usually use thread pools, read-write locks, message queues and other technologies and frameworks. The underlying principle is queues, so we must not underestimate queues. The foundation of the API and learning the queues are very important for learning Java deeply.

This article mainly takes the LinkedBlockingQueue queue as an example to describe the specific implementation of the bottom layer in detail.

1 Overall architecture

LinkedBlockingQueue Chinese is called a linked list blocking queue. This name is very good. From the naming, it is known that the underlying data structure is a linked list, and the queue is blockable. Next, we will look at LinkedBlockingQueue from the overall structure.

1.1 Class diagram

First, let's take a look at the LinkedBlockingQueue class diagram, as follows:
Insert picture description here
From the class diagram, we can probably see two paths:

  1. AbstractQueue-> AbstractCollection-> Collection-> Iterable This path dependency is mainly to reuse some operations of Collection and iterator. When we talk about collections, we all know what these classes do and what we can do, so we wo n’t elaborate. Over
  2. BlockingQueue-> Queue-> Collection, BlockingQueue and Queue are two new interfaces, let's focus on it.

Queue is the most basic interface. Almost all queue implementation classes will implement this interface. This interface defines three types of operations for queues:
new operations:

  1. An exception is thrown when the add queue is full;
  2. false when the offer queue is full.
    View and delete operations:
  3. An exception is thrown when the remove queue is empty;
  4. When the poll queue is empty, it returns null.
    Only view without deleting:
  5. An exception is thrown when the element queue is empty;
  6. When the peek queue is empty, null is returned.

A total of 6 methods, in addition to the above classification methods, can also be divided into two categories:

  1. When the queue is full or empty, throw an exception, such as add, remove, element;
  2. When the queue is full or empty, return special values, such as offer, poll, peek.

In fact, these are more difficult to remember. Every time I need to use it, I will look at the source code to remember whether this method throws an exception or returns a special value.

BlockingQueue adds the concept of blocking on the basis of Queue, such as blocking all the time, or blocking for a period of time. To facilitate memory, we draw a table as follows:

operating Throw an exception Special value Keep blocking Block for a while
New operation-queue full add offer returns false put Offer returns false after timeout
View and delete operations-queue empty remove poll returns null take poll returns null after timeout
Only view and do not delete operations-the queue is empty element peek returns null No No

PS: The remove method, as defined in the BlockingQueue class annotation, throws an exception, but the remove method in LinkedBlockingQueue actually returns false.

As you can see from the table, BlockingQueue adds blocking in the two major operations of adding, viewing, and deleting, and you can choose to block all the time, or return to a special value after blocking for a while.

1.2 Class notes

Let's see what information we can get from the class annotation of LinkedBlockingQueue:

  1. Based on a linked list blocking queue, the underlying data structure is a linked list;
  2. The linked list maintains the first-in-first-out queue, the new elements are placed at the end of the team, and the acquired elements are taken from the head of the team;
  3. The size of the linked list can be set during initialization, and the default is the maximum value of Integer;
  4. You can use all operations of the two interfaces of Collection and Iterator, because the interface of the two is implemented.

1.3 Internal composition

The internal structure of LinkedBlockingQueue is simply divided into three parts: 链表存储 + 锁 + 迭代器Let us look at the source code.

// 链表结构 begin
//链表的元素
static class Node<E> {
    E item;
 
    //当前元素的下一个,为空表示当前节点是最后一个
    Node<E> next;
 
    Node(E x) { item = x; }
}
 
//链表的容量,默认 Integer.MAX_VALUE
private final int capacity;
 
//链表已有元素大小,使用 AtomicInteger,所以是线程安全的
private final AtomicInteger count = new AtomicInteger();
 
//链表头
transient Node<E> head;
 
//链表尾
private transient Node<E> last;
// 链表结构 end
 
// 锁 begin
//take 时的锁
private final ReentrantLock takeLock = new ReentrantLock();
 
// take 的条件队列,condition 可以简单理解为基于 ASQ 同步机制建立的条件队列
private final Condition notEmpty = takeLock.newCondition();
 
// put 时的锁,设计两把锁的目的,主要为了 take 和 put 可以同时进行
private final ReentrantLock putLock = new ReentrantLock();
 
// put 的条件队列
private final Condition notFull = putLock.newCondition();
// 锁 end
 
// 迭代器 
// 实现了自己的迭代器
private class Itr implements Iterator<E> {
………………
}

From the point of view of the code, the structure is very clear, and the three structures do their job:

  1. The role of the linked list is to save the current node. The data in the node can be anything. It is a generic type. For example, when the queue is applied to the thread pool, the node is the thread. For example, the queue is applied to the message queue, and the node is the message. The meaning of the node mainly depends on the scene where the queue is used;
  2. There are take locks and put locks to ensure thread safety during queue operations. Two types of locks are designed so that take and put operations can be performed simultaneously without affecting each other.

1.4 Initialization

There are three ways to initialize:

  1. Specify the size of the linked list;
  2. Do not specify the size of the linked list, the default is the maximum value of Integer;
  3. Initialize existing collection data.

The source code is as follows:

// 不指定容量,默认 Integer 的最大值
public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE);
}
// 指定链表容量大小,链表头尾相等,节点值(item)都是 null
public LinkedBlockingQueue(int capacity) {
    if (capacity <= 0) throw new IllegalArgumentException();
    this.capacity = capacity;
    last = head = new Node<E>(null);
}
 
// 已有集合数据进行初始化
public LinkedBlockingQueue(Collection<? extends E> c) {
    this(Integer.MAX_VALUE);
    final ReentrantLock putLock = this.putLock;
    putLock.lock(); // Never contended, but necessary for visibility
    try {
        int n = 0;
        for (E e : c) {
            // 集合内的元素不能为空
            if (e == null)
                throw new NullPointerException();
            // capacity 代表链表的大小,在这里是 Integer 的最大值
            // 如果集合类的大小大于 Integer 的最大值,就会报错
            // 其实这个判断完全可以放在 for 循环外面,这样可以减少 Integer 的最大值次循环(最坏情况)
            if (n == capacity)
                throw new IllegalStateException("Queue full");
            enqueue(new Node<E>(e));
            ++n;
        }
        count.set(n);
    } finally {
        putLock.unlock();
    }
}

For the initialization source code, we explain two points:

  1. During initialization, the size of the capacity will not affect performance, only affect the later use, because the initialization queue is too small, it is easy to cause the error that the queue is full without reporting how much;
  2. When initializing a given set of data, the source code gives an inelegant demonstration. We are not opposed to checking whether the size of the current linked list exceeds the capacity at each for loop, but we hope to start before the for loop begins. Do one step of this kind of work. For example, the given set size is 1 w and the linked list size is 9k. According to the current code implementation, it can only be found when the for loop is 9k times. The original size of the given set is already greater than the linked list size, resulting in 9k cycles. It is a waste of resources. It is better to check once before the for loop. If 1w> 9k, just report the error.

2 New blocking

There are many new methods, such as: add, put, offer, the difference between the three is mentioned above. Let's take the put method as an example. The put method will block until the queue is full, and will continue to execute until the queue is not full and when it is woken up. The source code is as follows:

// 把e新增到队列的尾部。
// 如果有可以新增的空间的话,直接新增成功,否则当前线程陷入等待
public void put(E e) throws InterruptedException {
    // e 为空,抛出异常
    if (e == null) throw new NullPointerException();
    // 预先设置 c 为 -1,约定负数为新增失败
    int c = -1;
    Node<E> node = new Node<E>(e);
    final ReentrantLock putLock = this.putLock;
    final AtomicInteger count = this.count;
    // 设置可中断锁
    putLock.lockInterruptibly();
    try {
        // 队列满了
        // 当前线程阻塞,等待其他线程的唤醒(其他线程 take 成功后就会唤醒此处被阻塞的线程)
        while (count.get() == capacity) {
            // await 无限等待
            notFull.await();
        }
 
        // 队列没有满,直接新增到队列的尾部
        enqueue(node);
 
        // 新增计数赋值,注意这里 getAndIncrement 返回的是旧值
        // 这里的 c 是比真实的 count 小 1 的
        c = count.getAndIncrement();
 
        // 如果链表现在的大小 小于链表的容量,说明队列未满
        // 可以尝试唤醒一个 put 的等待线程
        if (c + 1 < capacity)
            notFull.signal();
 
    } finally {
        // 释放锁
        putLock.unlock();
    }
    // c==0,代表队列里面有一个元素
    // 会尝试唤醒一个take的等待线程
    if (c == 0)
        signalNotEmpty();
}
// 入队,把新元素放到队尾
private void enqueue(Node<E> node) {
    last = last.next = node;
}

From the source code we can summarize the following points:

  1. To add data to the queue, the first step is to lock, so the new data is thread safe;
  2. The new data in the queue can be simply added to the end of the linked list;
  3. When adding, if the queue is full, the current thread will be blocked. The bottom layer of blocking is the ability to lock. The bottom layer implementation is also related to the queue. The principle we will talk about in the lock chapter;
  4. After the new data is added successfully, at the appropriate time, the waiting thread of put (when the queue is not full) or the waiting thread of take (when the queue is not empty) will be awakened. Block the thread and continue to run, ensuring that the time to evoke is not wasted.

The above is the principle of the put method. As for the offer method blocking for more than one time, it is still unsuccessful, and it will directly return to the implementation of the default value. Compared with the put method, only a few lines of code have been modified.
Insert picture description here

3 Block delete

There are many ways to delete, we mainly look at two key issues:

  1. What is the principle of deletion;
  2. How to realize the difference between view and delete and only view and not delete.

First, let's look at the first problem. Let's take the take method as an example to explain the underlying source code that is viewed and deleted :

// 阻塞拿数据
public E take() throws InterruptedException {
    E x;
    // 默认负数,代表失败
    int c = -1;
    // count 代表当前链表数据的真实大小
    final AtomicInteger count = this.count;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lockInterruptibly();
    try {
        // 空队列时,阻塞,等待其他线程唤醒
        while (count.get() == 0) {
            notEmpty.await();
        }
        // 非空队列,从队列的头部拿一个出来
        x = dequeue();
        // 减一计算,注意 getAndDecrement 返回的值是旧值
        // c 比真实的 count 大1
        c = count.getAndDecrement();
        
        // 如果队列里面有值,从 take 的等待线程里面唤醒一个。
        // 意思是队列里面有值啦,唤醒之前被阻塞的线程
        if (c > 1)
            notEmpty.signal();
    } finally {
        // 释放锁
        takeLock.unlock();
    }
    // 如果队列空闲还剩下一个,尝试从 put 的等待线程中唤醒一个
    if (c == capacity)
        signalNotFull();
    return x;
}
// 队头中取数据
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;// 头节点指向 null,删除
    return x;
}

The overall process is very similar to put, which is to first lock and then take the data from the head of the queue. If the queue is empty, it will block until the queue has value.

It is simpler to view the elements without deleting them. Just take out the data at the head of the queue. We take peek as an example. The source code is as follows:

// 查看并不删除元素,如果队列为空,返回 null
public E peek() {
    // count 代表队列实际大小,队列为空,直接返回 null
    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();
    }
}

It can be seen that the logic of taking data from the team head is not consistent between viewing and deleting, and not deleting, which leads to one deleting and the other not deleting team head data.

4 Summary

This article introduces the linked list queue through the source code of LinkedBlockingQueue. When the queue is full and empty, what happens to the queue when adding and deleting data?

The queue itself is a blocking tool. We can apply this tool to various blocking scenarios. For example, the queue is applied to the thread pool. When the thread pool runs out, we put all new requests in the blocking queue and wait; the queue application To the message queue, when the consumer's processing capacity is limited, we can put the message in the queue and wait for the consumer to consume slowly; each time it is applied to a new scenario, it is a new technical tool, so learn the queue Useful.

Published 40 original articles · won praise 1 · views 4986

Guess you like

Origin blog.csdn.net/aha_jasper/article/details/105523782