7. Detailed explanation of Condition & Queue system analysis of concurrent programming

1. BlockingQueue blocking queue

In a multi-threaded environment, data sharing can be easily realized through queues. For example, in the classic "producer" and "consumer" models, data sharing between the two can be easily realized through queues. Suppose we have several producer threads and several consumer threads. If the producer thread needs to share the prepared data with the consumer thread, the data sharing problem between them can be easily solved by using the queue to transfer the data. But what if the data processing speed does not match between the producer and the consumer within a certain period of time? Ideally, if the rate at which the producer produces data is greater than the rate at which the consumer consumes it, and when the produced data accumulates to a certain extent, the producer must pause and wait (block the producer thread) to wait for the consumer The thread finishes processing the accumulated data, and vice versa. However, before the release of the concurrent package, in a multi-threaded environment, each of our programmers had to control these details by themselves, especially taking into account efficiency and thread safety, which would bring a lot of complexity to our programs . Fortunately, at this time, a powerful concurrent package was born, and he also brought us a powerful BlockingQueue

The core method of BlockingQueue
1. Put data

(1) offer(anObject): Indicates that if possible, add anObject to the BlockingQueue, that is, if the BlockingQueue can accommodate, return true, otherwise return false. (This method does not block the current execution method

thread);      
(2) offer(E o, long timeout, TimeUnit unit): You can set the waiting time, if the BlockingQueue cannot be added to the queue within the specified time, it will return failure.

(3) put(anObject): add anObject to the BlockingQueue, if there is no space in the BlockQueue, the thread calling this method will be blocked until there is space in the BlockingQueue before continuing.

2. Get data

(1) poll(time): Take away the first object in the BlockingQueue, if it cannot be taken out immediately, you can wait for the time specified by the time parameter, and return null if it cannot be taken out;

(2) poll(long timeout, TimeUnit unit): Take out an object at the head of the queue from the BlockingQueue. If there is data in the queue within the specified time, the data in the queue will be returned immediately. Otherwise, if there is no data available after the timeout, return failure.

(3) take(): Take away the first object in the BlockingQueue, if the BlockingQueue is empty, block and enter the waiting state until new data is added to the BlockingQueue;

(4) drainTo(): ​​Obtain all available data objects from the BlockingQueue at one time (you can also specify the number of acquired data). This method can improve the efficiency of acquiring data; there is no need to lock or release locks in batches multiple times.

Common BlockingQueue
insert image description here

  1. ArrayBlockingQueue implements an array-based blocking queue. Inside ArrayBlockingQueue, a fixed-length array is maintained to cache data objects in the queue. This is a commonly used blocking queue. In addition to a fixed-length array, ArrayBlockingQueue also stores two integers Variables, which respectively identify the position of the head and tail of the queue in the array.
      ArrayBlockingQueue shares the same lock object when the producer puts data and the consumer gets data, which also means that the two cannot really run in parallel, which is especially different from LinkedBlockingQueue; according to the analysis of the implementation principle, ArrayBlockingQueue can be used Separate locks, allowing full parallelism of producer and consumer operations. The reason why Doug Lea didn't do this may be because the data writing and fetching operations of ArrayBlockingQueue are already light enough to introduce an independent locking mechanism. In addition to bringing additional complexity to the code, it does not account for the performance at all. to any cheap. Another obvious difference between ArrayBlockingQueue and LinkedBlockingQueue is that the former will not generate or destroy any additional object instances when inserting or deleting elements, while the latter will generate an additional Node object. In a system that needs to process large batches of data efficiently and concurrently for a long time, its impact on GC is still somewhat different. When creating ArrayBlockingQueue, we can also control whether the internal lock of the object adopts fair lock, and the default adopts unfair lock
public class ArrayBlockingQueueTest {
    
    
    /**
     * 创建容量大小为1的有界队列
     */
    private BlockingQueue<Ball> blockingQueue = new ArrayBlockingQueue<Ball>(1);

    /**
     * 队列大小
     * @return
     */
    public int queueSize(){
    
    
        return blockingQueue.size();
    }

    /**
     * 将球放入队列当中,生产者
     * @param ball
     * @throws InterruptedException
     */
    public void produce(Ball ball) throws InterruptedException{
    
    
        blockingQueue.put(ball);
    }

    /**
     * 将球从队列当中拿出去,消费者
     * @return
     */
    public Ball consume() throws InterruptedException {
    
    
       return blockingQueue.take();
    }

    public static void main(String[] args){
    
    
        final ArrayBlockingQueueTest box = new ArrayBlockingQueueTest();
        ExecutorService executorService = Executors.newCachedThreadPool();

        /**
         * 往箱子里面放入乒乓球
         */
        executorService.submit(new Runnable() {
    
    
            public void run() {
    
    
                int i = 0;
                while (true){
    
    
                    Ball ball = new Ball();
                    ball.setNumber("乒乓球编号:"+i);
                    ball.setColor("yellow");
                    try {
    
    
                        System.out.println(System.currentTimeMillis()+
                                ":准备往箱子里放入乒乓球:--->"+ball.getNumber());
                        box.produce(ball);
                        System.out.println(System.currentTimeMillis()+
                                ":往箱子里放入乒乓球:--->"+ball.getNumber());
                        System.out.println("put操作后,当前箱子中共有乒乓球:--->"
                                + box.queueSize() + "个");
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                    i++;
                }
            }
        });

        /**
         * consumer,负责从箱子里面拿球出来
         */
        executorService.submit(new Runnable() {
    
    
            public void run() {
    
    
                while (true){
    
    
                    try {
    
    
                        System.out.println(System.currentTimeMillis()+
                                "准备到箱子中拿乒乓球:--->");
                        Ball ball = box.consume();
                        System.out.println(System.currentTimeMillis()+
                                "拿到箱子中的乒乓球:--->"+ball.getNumber());
                        System.out.println("take操作后,当前箱子中共有乒乓球:--->"
                                + box.queueSize() + "个");
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
            }
        });

    }

}

public class Ball {
    
    
    /**
     * 编号
     */
    private String number ;
    /**
     * 颜色
     */
    private String color ;

    public String getNumber() {
    
    
        return number;
    }

    public void setNumber(String number) {
    
    
        this.number = number;
    }

    public String getColor() {
    
    
        return color;
    }

    public void setColor(String color) {
    
    
        this.color = color;
    }
}

  1. LinkedBlockingQueue is a blocking queue based on a linked list. Similar to ArrayListBlockingQueue, it also maintains a data buffer queue (the queue is composed of a linked list). When the producer puts a piece of data into the queue, the queue will get the data from the producer. And cached inside the queue, and the producer returns immediately; only when the queue buffer reaches the maximum buffer capacity (LinkedBlockingQueue can specify this value through the constructor), the producer queue will be blocked until the consumer consumes a block from the queue The producer thread will be woken up if the data is shared, and vice versa, the processing on the consumer side is also based on the same principle. The reason why LinkedBlockingQueue can efficiently process concurrent data is also because it uses independent locks for the producer and consumer to control data synchronization, which also means that producers and consumers can run in parallel under high concurrency Operate the data in the queue in a timely manner to improve the concurrency performance of the entire queue.

  2. The elements in the DelayQueue can only be obtained from the queue when the specified delay time is up. DelayQueue is a queue with no size limit, so the operation of inserting data into the queue (producer) will never be blocked, and only the operation of obtaining data (consumer) will be blocked.

public class DelayedQueueTest {
    
    

    public static void main(String[] args) {
    
    
        DelayQueue<MovieTiket> delayQueue = new DelayQueue<MovieTiket>();
        MovieTiket tiket = new MovieTiket("电影票0",10000);
        delayQueue.put(tiket);
        MovieTiket tiket1 = new MovieTiket("电影票1",5000);
        delayQueue.put(tiket1);
        MovieTiket tiket2 = new MovieTiket("电影票2",8000);
        delayQueue.put(tiket2);
        System.out.println("message:--->入队完毕");

        while( delayQueue.size() > 0 ){
    
    
            try {
    
    
                tiket = delayQueue.take();
                System.out.println("电影票出队:"+tiket.getMsg());
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    }

}

public class MovieTiket implements Delayed {
    
    
    //延迟时间
    private final long delay;
    //到期时间
    private final long expire;
    //数据
    private final String msg;
    //创建时间
    private final long now;

    public long getDelay() {
    
    
        return delay;
    }

    public long getExpire() {
    
    
        return expire;
    }

    public String getMsg() {
    
    
        return msg;
    }

    public long getNow() {
    
    
        return now;
    }

    /**
     * @param msg 消息
     * @param delay 延期时间
     */
    public MovieTiket(String msg , long delay) {
    
    
        this.delay = delay;
        this.msg = msg;
        expire = System.currentTimeMillis() + delay;    //到期时间 = 当前时间+延迟时间
        now = System.currentTimeMillis();
    }

    /**
     * @param msg
     */
    public MovieTiket(String msg){
    
    
        this(msg,1000);
    }

    public MovieTiket(){
    
    
        this(null,1000);
    }

    /**
     * 获得延迟时间   用过期时间-当前时间,时间单位毫秒
     * @param unit
     * @return
     */
    public long getDelay(TimeUnit unit) {
    
    
        return unit.convert(this.expire
                - System.currentTimeMillis() , TimeUnit.MILLISECONDS);
    }

    /**
     * 用于延迟队列内部比较排序  当前时间的延迟时间 - 比较对象的延迟时间
     * 越早过期的时间在队列中越靠前
     * @param delayed
     * @return
     */
    public int compareTo(Delayed delayed) {
    
    
        return (int) (this.getDelay(TimeUnit.MILLISECONDS)
                - delayed.getDelay(TimeUnit.MILLISECONDS));
    }

    @Override
    public String toString() {
    
    
        return "MovieTiket{" +
                "delay=" + delay +
                ", expire=" + expire +
                ", msg='" + msg + '\'' +
                ", now=" + now +
                '}';
    }
}

  1. PriorityBlockingQueue is a priority-based blocking queue (priority is determined by the Compator object passed in by the constructor), but it should be noted that PriorityBlockingQueue does not block data producers, but only blocks when there is no consumable data consumers of data. Therefore, special attention should be paid when using it. The speed at which producers can produce data must not be faster than the speed at which consumers can consume data. Otherwise, over time, all available heap memory space will eventually be exhausted. When implementing PriorityBlockingQueue, the internal control thread synchronization lock uses a fair lock.

  2. SynchronousQueue is an unbuffered waiting queue, similar to direct transactions without intermediaries. It is a bit like producers and consumers in primitive society. Producers take products to the market to sell to final consumers of products, and consumers must personally Go to the bazaar to find the direct producers of the goods you want. If one party does not find a suitable target, then sorry, everyone is waiting in the bazaar. Compared with the buffered BlockingQueue, there is one less intermediate distributor link (buffer zone). If there is a distributor, the producer directly wholesales the products to the distributor, regardless of whether the distributor will eventually sell these products to those Consumers, because dealers can stock a part of the goods, compared with the direct transaction model, the overall throughput of the intermediate dealer model will be higher (it can be bought and sold in batches); but on the other hand, because of the introduction of dealers, An additional transaction link is added between the product from the producer to the consumer, and the timely response performance of a single product may be reduced.

2. Condition mechanism

Condition only appeared in java 1.5. It is used to replace the traditional Object's wait() and notify() to realize the cooperation between threads. Compared with using Object's wait() and notify(), using Condition's await() , signal() is a safer and more efficient way to achieve inter-thread collaboration. Simply put, its function is to make some threads wait for a certain condition (Condition) together. Only when the condition is met (signal or signalAll method is called), these waiting threads will be awakened to re-compete for the lock. wait() and notify() are more inclined to the underlying implementation development, while the Condition interface is more inclined to the waiting notification effect of code implementation. The differences and commonalities between the two can also be understood:insert image description here

basic use

package cn.memedai;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Lock与Condition接口示例
 */
public class LockConditionDemo {
    
    

    //存储地方
    class Depot {
    
    
        private int capacity;
        private int size;
        private Lock lock;
        private Condition fullCondition;
        private Condition emptyCondition;

        public Depot(int capacity) {
    
    
            this.capacity = capacity;
            this.size = 0;
            this.lock = new ReentrantLock();
            this.fullCondition = lock.newCondition();
            this.emptyCondition = lock.newCondition();
        }

        //生产操作
        public void produce(int newSize) throws InterruptedException {
    
    
            lock.lock();
            int left = newSize;
            try {
    
    
                while (left > 0) {
    
    
                    //代表超过了容量就不能再生产了
                    while (size >= capacity) {
    
    
                        fullCondition.await();//进行等待处理
                    }
                    //获取实际生产的数量(及库存中新增的数量)
                    //如果库存+要生产的大于了总的容量那么新增的就是总容量的数量相减
                    int inc = (size + left) > capacity ? (capacity - size) : left;
                    size += inc;
                    left -= inc;
                    System.out.println(Thread.currentThread().getName() + "------left剩余" + left + "------size容量" + size + "-------inc增长" + inc);
                    emptyCondition.signal();
                }
            } finally {
    
    
                lock.unlock();//解锁
            }
        }

        //消费操作
        public void consume(int newSize) throws InterruptedException {
    
    
            lock.lock();
            try {
    
    
                int left = newSize;
                while (left > 0) {
    
    
                    //库存为0等待生产者进行生产的操作
                    while (size <= 0) {
    
    
                        emptyCondition.await();
                    }
                    int dec = (size < left) ? size : left;
                    size -= dec;
                    left -= dec;
                    System.out.println(Thread.currentThread().getName() + "-------left剩余" + left + "-------size容量" + size + "--------减少量dec" + dec);
                    fullCondition.signal();
                }
            } finally {
    
    
                lock.unlock();
            }
        }
    }

    //生产者
    class Producer{
    
    
        private Depot depot;

        public Producer(Depot depot) {
    
    
            this.depot = depot;
        }

        //往存储地方生产
        public void produce(final int newSize){
    
    
            new Thread(){
    
    
                @Override
                public void run() {
    
    
                    try {
    
    
                        depot.produce(newSize);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
            }.start();
        }
    }
   //消费者
    class Customer{
    
    
        private  Depot depot;

        public Customer(Depot depot) {
    
    
            this.depot = depot;
        }
     //进行消费
        public void consume(final int newSize){
    
    
            new Thread(){
    
    
                @Override
                public void run() {
    
    
                    try {
    
    
                        depot.consume(newSize);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
            }.start();
        }
    }

    public static void main(String[] args) {
    
    
        Depot depot = new LockConditionDemo().new Depot(100);
        Producer producer = new LockConditionDemo().new Producer(depot);
        Customer customer = new LockConditionDemo().new Customer(depot);
        producer.produce(60);
        producer.produce(120);
        customer.consume(90);
        customer.consume(150);
        producer.produce(110);
    }
}

print result

Thread-1------left剩余20------size容量100-------inc增长100
Thread-2-------left剩余0-------size容量10--------减少量dec90
Thread-3-------left剩余140-------size容量0--------减少量dec10
Thread-4------left剩余10------size容量100-------inc增长100
Thread-3-------left剩余40-------size容量0--------减少量dec100
Thread-4------left剩余0------size容量10-------inc增长10
Thread-3-------left剩余30-------size容量0--------减少量dec10
Thread-1------left剩余0------size容量20-------inc增长20
Thread-3-------left剩余10-------size容量0--------减少量dec20
Thread-0------left剩余0------size容量60-------inc增长60
Thread-3-------left剩余0-------size容量50--------减少量dec10

Through a simple example, there are two conditions for using Condition. First, the thread must obtain the current synchronization state, and second, the Condition object must be obtained from the lock, and the condition.await() method corresponds to the Object.wait() method. The current thread waits when certain conditions are met, and condition.signal() wakes up the current thread under certain conditions. It is very convenient to use with the lock interface.

The implementation principle of Condition waiting/notification mechanism
insert image description here
await(): make the current thread enter the waiting state until it is awakened by signal(), signalAll() method or interrupted
signal(): wake up a thread in waiting
signalAll(): wake up the waiting thread The Condition interface of all threads
only defines the relevant methods for processing waiting notifications. The ConditionObject class in AQS that truly realizes the effect of waiting for notifications. Before understanding the source code, let’s talk about synchronization queues and waiting queues
: When the synchronization state is obtained, a Node node will be created and put at the end of the synchronization queue, and all threads entering the synchronization queue will be blocked.
In AQS, both the synchronization queue and the waiting queue reuse the node class Node. A synchronization state can contain multiple waiting queues, and the waiting queue is just a one-way queue. await(): make the current thread
insert image description here
enter the waiting state

public final void await() throws InterruptedException {
    
    
            if (Thread.interrupted())//响应中断
                throw new InterruptedException();
            Node node = addConditionWaiter();//放入到等待队列中
            int savedState = fullyRelease(node);//释放同步状态(同步队列头节点释放状态唤醒后继节点获取同步状态)
            int interruptMode = 0;         //判断是否在同步队列中
            while (!isOnSyncQueue(node)) {
    
    
                LockSupport.park(this);//存在等待队列中就阻塞该线程
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)//判断等待过程中是否被中断过
                    break;
            }          //自旋去获取同步状态【在AQS中了解】获取成功并且在退出等待时不抛出中断异常(抛出了异常就会立马被中断)
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;//在退出等待时重新中断
            if (node.nextWaiter != null) //如果存在其他节点
                unlinkCancelledWaiters();//移除所有不是等待状态的节点
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);//如果在等待过程中发现被中断,就执行中断的操作
        }

addConditionWaiter(): Add elements to the waiting queue

      private Node addConditionWaiter() {
    
    
             Node t = lastWaiter;//等待队列中的最后一个元素
            if (t != null && t.waitStatus != Node.CONDITION) {
    
    //如果尾节点部位null,并且尾节点不是等待状态中说明这个节点不应该待在等待队列中
                unlinkCancelledWaiters();//从等待队列中移除
                t = lastWaiter;
            }
            Node node = new Node(Thread.currentThread(), Node.CONDITION);//创建一个等待状态的节点
           if (t == null)
                 firstWaiter = node;
             else
                t.nextWaiter = node;
            lastWaiter = node;//加入等待队列的尾部
            return node;
         }

unlinkCancelledWaiters(): Remove nodes that are not in the waiting state from the waiting queue

private void unlinkCancelledWaiters() {
    
    
            Node t = firstWaiter;//头节点
            Node trail = null;
            while (t != null) {
    
    //存在节点
                Node next = t.nextWaiter;//下一个节点
                if (t.waitStatus != Node.CONDITION) {
    
    //如果不是出于等待中的状态
                    t.nextWaiter = null;//t的后指针引用清除
                    if (trail == null)//前面是否存在节点
                        firstWaiter = next;//下一个节点就是头节点
                    else
                        trail.nextWaiter = next;//赋值给前节点的后指针引用
                    if (next == null)//代表不存在元素了
                        lastWaiter = trail;
                }
                else
                    trail = t;//将t赋值给trail
                t = next;//next赋值给t
            }
        }

fullyRelease(Node node): Release the current state value and return to the synchronization state

final int fullyRelease(Node node) {
    
    
        boolean failed = true;//失败状态
        try {
    
    
            int savedState = getState();//获取当前同步状态值
            if (release(savedState)) {
    
    //独占模式下释放同步状态,AQS独占式释放锁、前面文章讲过
                failed = false;//失败状态为false
                return savedState;//返回同步状态
            } else {
    
    
                throw new IllegalMonitorStateException();
            }
        } finally {
    
    
            if (failed)
                node.waitStatus = Node.CANCELLED;//取消等待状态
        }
    }

isOnSyncQueue: Determine whether the thread is in the synchronization queue

final boolean isOnSyncQueue(Node node) {
    
    
        if (node.waitStatus == Node.CONDITION || node.prev == null)//如果等待状态为等待中,或者前继节点为null代表第一种情况该节点出于等待状态,第二种情况可能已经被唤醒不在等待队列中了
            return false;
        if (node.next != null) //如果后继节点不为null代表肯定在等待队列中
            return true;
        return findNodeFromTail(node);//从后往前找判断是否在等待队列中
    }

To sum up the waiting operation:
First, the waiting operation does not perform CAS or any synchronization operation, because the thread that calls the await() method is the thread that acquires the current lock object, that is, the first node in the synchronization queue. After calling the await() method, Create a waiting node for the first node of the synchronization queue and put it at the end of the waiting queue, then release the synchronization state (if the synchronization state is not released, it will cause deadlock), wake up the successor nodes in the synchronization queue, and then the current thread enters the waiting state
insert image description here
signal (): Wake up a thread in the waiting queue

public final void signal() {
    
    
            if (!isHeldExclusively())//判断当前线程是否已经获取同步状态
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;//等待队列头节点
            if (first != null)
                doSignal(first);//具体实现方法唤醒第一个node
        }

doSignal(Node node): Specifically handle the operation of waking up the node

private void doSignal(Node first) {
    
    
            do {
    
    
                if((firstWaiter = first.nextWaiter) == null)//执行移除头节点的操作
                    lastWaiter = null;
                first.nextWaiter = null;
            } while (!transferForSignal(first) && (first = firstWaiter) != null);
        }

transferForSignal(Node node): the specific implementation of wake-up

final boolean transferForSignal(Node node) {
    
    
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))//将节点的等待状态设置更改为初始状态如果改变失败就会被取消
            return false;
        Node p = enq(node);//往同步队列中添加节点【死循环方式】
        int ws = p.waitStatus;//获取节点的等待状态
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))//如果该结点的状态为cancel 或者修改waitStatus失败,则直接唤醒(这一步判断是为了不立刻唤醒脱离等待中的线程,因为他要等同步队列中的头节点释放同步状态再去竞争)
            LockSupport.unpark(node.thread);//具体的唤醒操作
        return true;
    }

Summarize the flow of the wake-up operation:
when the signal() method is called, the first node in the waiting queue is taken out and added to the synchronization queue. At this time, the node will not be woken up immediately because even if it is woken up, it needs to re-obtain synchronization state, but after calling the lock.unlock() method to release the lock, wake it up to obtain the synchronization state.
insert image description here
Up to now, the basic Condition waiting notification mechanism has been explained. As for the additional functions such as timeout waiting or waking up all functions in The source code is not much different. A little new function needs to be added, and some processing logic has been added to the original await() method. The real principle is still almost the same.

Guess you like

Origin blog.csdn.net/qq_39513430/article/details/110557494