java and multithreading concurrency principle learning notes

Posted by Elangler on Sat, 26 Feb 2022 09:20:01 +0100

Blocking queue

Blocking queues are often used in producer consumer scenarios. In java8, JUC provides seven blocking queues.

Class nameeffect
ArrayBlockingQueueThe bounded blocking queue implemented by the array sorts the elements according to the first in first out (FIFO) principle.
LinkedBlockingQueueA bounded blocking queue implemented by a linked list. The default and maximum length of this queue is integer MAX_ VALUE. This queue sorts elements on a first in, first out basis
PriorityBlockingQueueUnbounded blocking queue that supports prioritization. By default, elements are arranged in natural ascending order. You can also customize the class to implement the compareTo() method to specify the element arrangement rules, or specify the construction parameter Comparator to sort the elements when initializing PriorityBlockingQueue.
DelayQueueUnbounded blocking queue implemented by priority queue
SynchronousQueueDo not store the blocking queue of elements. Each put operation must wait for a take operation, otherwise you cannot continue to add elements.
LinkedTransferQueueUnbounded blocking queue implemented by linked list
LinkedBlockingDequeBidirectional blocking queue implemented by linked list

Operation method of blocking queue

Insert operation
add(e): add elements to the queue. If the queue is full, an error will be reported if you continue to insert elements, such as IllegalStateException.
offer(e): when adding an element to the queue, it will return the status of whether the element was successfully inserted. If it is successful, it will return true
put(e): when the blocking queue is full, the producer continues to add elements through put, and the queue will block the producer thread until the queue is available
offer(e,time,unit): if you continue to add elements after the blocking queue is full, the producer thread will be blocked for a specified time. If it times out, the thread will exit directly
Remove operation
remove(): when the queue is empty, calling remove will return false. If the element is removed successfully, it will return true
poll(): when there are elements in the queue, an element will be taken from the queue. If the queue is empty, null will be returned directly
take(): get the elements in the queue based on blocking. If the queue is empty, the take method will block until there is new data in the queue that can be consumed
poll(time,unit): get data with timeout mechanism. If the queue is empty, it will wait for the specified time to get the element return

Principle analysis of ArrayBlockingQueue

Construction method

ArrayBlockingQueue has three constructors.
capacity: indicates the length of the array, that is, the length of the queue.
Fair: indicates whether it is a fair blocking queue. By default, an unfair blocking queue is constructed
c: Indicates the initialization of the given data.

    public ArrayBlockingQueue(int capacity) {
        this(capacity, false);
    }

    
    public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        this.items = new Object[capacity];
        lock = new ReentrantLock(fair);
        notEmpty = lock.newCondition();
        notFull =  lock.newCondition();
    }

   
    public ArrayBlockingQueue(int capacity, boolean fair,
                              Collection<? extends E> c) {
        this(capacity, fair);

        final ReentrantLock lock = this.lock;
        lock.lock(); // Lock only for visibility, not mutual exclusion
        try {
            int i = 0;
            try {
                for (E e : c) {
                    checkNotNull(e);
                    items[i++] = e;
                }
            } catch (ArrayIndexOutOfBoundsException ex) {
                throw new IllegalArgumentException();
            }
            count = i;
            putIndex = (i == capacity) ? 0 : i;
        } finally {
            lock.unlock();
        }
    }

add method

    public boolean add(E e) {
        //Call the add method of the parent class, that is, abstractqueue
        return super.add(e);
    }
    //Method of abstractqueue class
    public boolean add(E e) {
        //Call the off method, and finally call back the offer method in ArrayBlockingQueue
        if (offer(e))
            return true;
        else
            throw new IllegalStateException("Queue full");
    }

    public boolean offer(E e) {
        checkNotNull(e);  //Judge whether the added data is empty
        final ReentrantLock lock = this.lock;
        lock.lock();  //Lock
        try {
            if (count == items.length)  // Judge the queue length. If the queue length is equal to the array length, it indicates that it is full and returns false directly
                return false;
            else {
                enqueue(e);  //enqueue is called to add an element to the queue
                return true;
            }
        } finally {
            lock.unlock();
        }
    }
    
    private void enqueue(E x) {
      
        final Object[] items = this.items;
        items[putIndex] = x;
        // When putIndex is equal to the length of the array, reset putIndex to 0
        //Because it is FIFO, when the elements in the queue are full, you need to start from scratch
        if (++putIndex == items.length)
            putIndex = 0;
        count++;  //Record the number of queue elements
        notEmpty.signal();//Wake up the thread in the waiting state, indicating that the element in the current queue is not empty. If there is a consumer thread blocking, you can start to take out the element
    }

put method

The function of the put method is the same as that of the add method. The difference is that the put method will block if the queue is full.

    public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        //This is also to obtain a lock, but the difference from lock is that this method preferentially allows other threads to call the interrupt method of the waiting thread to interrupt the wait
        //return. The lock method is to respond to the interrupt only after the attempt to obtain the lock is successful
        lock.lockInterruptibly();
        try {
            while (count == items.length)
                //When the queue is full, the current thread will be suspended by the notFull condition object and added to the waiting queue
                notFull.await();
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }

take method

The take method is a method of blocking the acquisition of elements in the queue. Its implementation principle is very simple. If there is a block, delete it or not. This blocking can be interrupted. If there is no data in the queue, join the notEmpty condition queue to wait (if there is data, take it directly and the method ends). If a new put thread adds data, the put operation will wake up the take thread and execute the take operation

	public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                //If the queue is empty, it will be blocked directly through await method. If there is a thread add or put, it will be blocked through notempty Signal () thread
                notEmpty.await();
            return dequeue();
        } finally {
            lock.unlock();
        }
    }
    
 	private E dequeue() {
        // assert lock.getHoldCount() == 1;
        // assert items[takeIndex] != null;
        final Object[] items = this.items;
        @SuppressWarnings("unchecked")
        E x = (E) items[takeIndex];
        items[takeIndex] = null;  //Set the element at this location to null
        //If you get the last element of the array, reset it to 0 and continue to get data from the head position
        if (++takeIndex == items.length)
            takeIndex = 0;
        count--;  //Decreasing number of queue elements
        if (itrs != null)
            itrs.elementDequeued();  //If the iterator is not empty, the data in the iterator is updated
        //Wake up a write thread that is blocked because the queue is full
        notFull.signal();
        return x;
    }

remove method

The remove method removes a specified element.

	public boolean remove(Object o) {
        if (o == null) return false;
        final Object[] items = this.items;
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            if (count > 0) {
                final int putIndex = this.putIndex;  //Gets the index of the next element to be added
                int i = takeIndex;
                do {
                    if (o.equals(items[i])) {  //Starting with the takeIndex subscript, find the element to be deleted
                        removeAt(i);  //Removes the specified element
                        return true;
                    }
                    if (++i == items.length)
                        i = 0;
                } while (i != putIndex);
            }
            return false;
        } finally {
            lock.unlock();
        }
    }

Principle diagram of ArrayBlockingQueue


When the thread executes the add or put method, it will put the current element into items[putIndex], and then putindex + 1 and count + 1. When the thread executes the take method, it will get items[takeIndex], and then take index + 1, count-1

Atomic operation class

The so-called atomicity means that one or more operations are either completed or none are executed. There can be no success and failure. For example, i=0; Executing i + + is divided into three steps. 1. Get the value of i, 2, i+1, 3, assign the result to i. When 10 threads execute i + + in parallel, the result may be less than 10. This is because when executing step 1, multiple threads may get the i value of 1 at the same time. This is a typical atomicity problem. It can be solved by locking it. And from jdk1 Starting from 5, JUC provides Atomic package, which provides Atomic operations for common data structures. It provides a simple, efficient, and thread safe way to update a variable.


Due to the relationship between variable types, 12 classes of atomic operations are provided in JUC. These 12 categories can be divided into four categories
1. Atomic update basic type
AtomicBoolean,AtomicInteger,AtomicLong
2. Update atom array
AtomicIntegerArray , AtomicLongArray ,AtomicReferenceArray
3. Atomic update reference
AtomicReference, AtomicReferenceFieldUpdater, AtomicMarkableReference (update reference type with marker bit)
5. Atomic update field
AtomicIntegerFieldUpdater,AtomicLongFieldUpdater,AtomicStampedReference

Topics: Java