Use of wait/notify and J.U.C Condition in synchronized thread communication and source code analysis

I remember there was a classic interview question: how to output from 1 to 100 in sequence with multiple threads?
The last chapter talked about the use and principle analysis of locks in Java. The above interview questions should be handy

This chapter mainly talks about the realization of production and consumption queue and Condition source code by thread communication in Java

Thread communication

The mutual exclusion of shared lock is used to realize the communication between two threads, so as to realize the production and consumption queue

1. Use Synchronized wait/notify to implement production and consumption queues

//Define a queue
static Queue<Integer> list = new LinkedList<>();
//Defines the size of the queue
static int size = 10;
//The production code is put into thread execution
public static void producer() throws InterruptedException {
    int i = 0;
    while (true) {
        i++;
        //Lock the queue
        synchronized (list) {
            if (list.size() == size) {
                System.out.println("The queue is full");
                //Wait when the queue is full, and consumers will wake up producers after consumption
                list.wait();
            }
            Thread.sleep(1000);
            list.add(i);
            System.out.println("Producer add:" + i);
            //After waking up the thread, it will be executed after the wait, which means that the execution can continue only after the lock is preempted
            list.notify();	
        }
    }
}
//Consumer code 
public static void comsume() throws InterruptedException {
    while (true) {
        synchronized (list) {
            if (list.size() == 0) {
                System.out.println("The queue is empty");
                //Wait when the queue is empty, and the producer will wake up the consumer when it produces
                list.wait();
            }
            Thread.sleep(1000);

            Integer remove = list.remove();
            System.out.println("Consumer consumption:" + remove);
            list.notify();
        }
    }
}
  • The synchronized implementation is based on the underlying source code. Let's not mention that there is also an implementation of JUC in Java. Let's have a look

2. Condition in j.u.c

  • signal/await is equivalent to synchronized notify/wait
  • It is also necessary to seize the lock
  • signal wakes up multiple threads. After waking up, you need to synchronize the competing lock. Those who fail to grab the lock need to synchronize to the AQS queue (be sure to understand the content in the previous article first)
  • await blocks the current thread, adds it to the waiting queue, and releases the lock
  • The difference with synchronized is that multiple queues can be used in Condition to put different threads

3. Implement production and consumption queue with Condition

Lock lock = new ReentrantLock();
//Different from synchronized, multiple queues can be used to put different threads in Condition
Condition addCondition = lock.newCondition();
Condition removeCondition = lock.newCondition();
int count = 10;
List<String> list = new ArrayList<>(count);

public void producer() {
    int i = 0;
    while (true) {
        lock.lock();
        i++;
        try {
            if (list.size() == count) {
                System.out.println("The queue is full");
                //Blocking the producer to release the lock will wake up the producer when the consumer consumes
                addCondition.await();
            }
            Thread.sleep(1000);
            list.add("abc"+i);
            System.out.println("Producer add:" + i);
            //Awaken consumers
            removeCondition.signal();
        } catch (Exception e) {
        } finally {
            lock.unlock();
        }

    }
}

public void comsume() {
    while (true) {
        lock.lock();
        try {
            if (list.size() == 0) {
                System.out.println("The queue is empty");
                //Blocking the consumer to release the lock will wake up when the producer adds
                removeCondition.await();
            }
            Thread.sleep(1000);
            System.out.println("Consumer consumption:"+list.get(0));
            list.remove(0);
            addCondition.signal();
        } catch (Exception e) {
        } finally {
            lock.unlock();
        }
    }
}

public static void main(String[] args) throws InterruptedException {
    ConditionDemo prod = new ConditionDemo();
    new Thread(() -> { prod.producer(); }).start();
    Thread.sleep(100);
    new Thread(() -> { prod.comsume(); }).start();
}

Write the code by yourself, or you'll forget it in the blink of an eye

4.Condition source code analysis

The Condition queue has its own one-way linked list waiting queue

  • await() blocks the current thread, adds it to the waiting queue, releases the lock (all, there may be reentry), and then synchronizes it to the AQS queue. There are operations to process interrupt (it may be interrupt ID wake-up, not Signal wake-up)
//AbstractQueuedSynchronizer#ConditionObject#await
public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    //Adding to the Condition waiting queue is similar to building an AQS queue, but this is a one-way queue with the status of CONDITION = -2;
    Node node = addConditionWaiter();
    //A fully released lock may re-enter
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    //Judge whether it is already in the AQS queue, and then return false to block the current thread according to the status of CONDITION
    //The signal wake-up will be synchronized to the AQS queue, and then the isOnSyncQueue will return true to jump out of the loop
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        //After waiting to be awakened, you should first deal with whether to interrupt the wake-up from here
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    //The next step is the content in AQS. Here, the content after getting the lock is thread safe
    //Re contention for lock savedState after wake-up / number of lock re entrances released
    //If not, it will continue to be blocked by parkAndCheckInterrupt and wait for the thread in the AQS queue to wake up
    if (acquireQueued(node,  savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        //Empty invalid thread
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

This is well understood, that is, a one-way linked list waiting queue is added in front of the original AQS queue.

  • Next, continue to analyze the wake-up source code Signal()
//AbstractQueuedSynchronizer#ConditionObject#signal
public final void signal() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    //The first waiting node
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

private void doSignal(Node first) {
    do {
        //Take out the next waiting node. The judgment condition is transferForSignal
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}

final boolean transferForSignal(Node node) {
	//Filter nodes with invalid status
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;

	//This method is very familiar with adding to the AQS queue
    Node p = enq(node);
    int ws = p.waitStatus;
    //If it is already in the pending state, wake up directly
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        //The thread that wakes up the Condition queue returns to LockSupport.park(this); Continue later
        LockSupport.unpark(node.thread);
    return true;
}

This source code looks very simple. If you don't understand it, read it several times and slowly understand it.

5. Practical application of condition

  • You can generate a blocking queue that you don't understand and wake up different threads based on your needs
  • Blocking queues, production consumers
  • Thread pool
  • Traffic cache

That is the whole content of this chapter.

Previous: J.U.C ReentrantLock use and source code analysis
Next: J. Blocking queue use and source code analysis in U.C – ArrayBlockingQueue

People with lofty ideals cherish the short days and worry about the long nights

Tags: Java Multithreading source code lock

Posted on Mon, 06 Dec 2021 23:08:14 -0500 by GoodWill