Deep understanding of Java concurrency framework AQS series (5): condition

Time:2021-5-13

Deep understanding of Java concurrency framework AQS series (1): threads
Deep understanding of Java concurrency framework AQS series (2): AQS framework introduction and lock concept
Deep understanding of Java concurrency framework AQS series (3): exclusive lock
Deep understanding of Java concurrency framework AQS series (4): shared lock
Deep understanding of Java concurrency framework AQS series (5): condition

1、 Preface

The conditional queue in AQS is more independent than the exclusive lock and shared lock in the previous paper. Even if there is no conditional queue, it has no effect on the systemReentrantLockSemaphoreClass, is conditional queue a dispensable product? The answer is No. let’s take a look at the JDK concurrency classes that directly or indirectly use conditional queues

  • ReentrantLock Exclusive lock classic class
  • ReentrantReadWriteLock Read write lock
  • ArrayBlockingQueue Blocking queue based on array
  • CyclicBarrier Loop fence to solve the problem of thread synchronization
  • DelayQueue Delay queue
  • LinkedBlockingDeque Bidirectional blocking queue
  • PriorityBlockingQueue Unbounded blocking queue with priority
  • ThreadPoolExecutor Thread pool constructor
  • ScheduledThreadPoolExecutor Thread pool constructor based on time scheduling
  • StampedLock Postmark lock, introduced after 1.8, more efficient read-write lock

Such a luxurious lineup, visibleConditionWe should not underestimate the status of

We briefly describe the function of conditional queueThere are three threads a, B and C, which are called respectivelywait/awaitMethod, the thread will block. If no other thread wakes up, the three threads will be blocked forever. At this time, if another thread callsnotify/signalThen one of a, B, C threads will be activated (according to the order in which it enters the condition queue) to execute the subsequent logic; If callednotifyAll/signalAllIf so, all three threads will be activated, which may be our simple understanding of conditional queue.Is this description accurate? probablyNot very rigorousWe introduce the condition queue of JDK to illustrate

Unified discourse: actually supported by grammarwait/notifyBoth AQS and JDK belong to the category of JDK, but in order to distinguish them, we define them as follows:

  • JDK conditional queueSyntax level supportwait/notify, i.eObjectIn classwait()/notify()method
  • AQS conditional queue: the condition queue provided by AQS, that is, the condition queue inside AQSConditionObjectclass

2、 Conditional queue in JDK (wait / notify)

As we all know, in JDK,wait/notify/notifyAllIs the root objectObjectAnd methods are defined asnativeLocal method

//Wait
public final native void wait(long timeout) throws InterruptedException;
//Wake up
public final native void notify();
//Wake up所有等待线程
public final native void notifyAll();

2.1、wait

//Step 1
synchronized (obj) {
  //Step 2
  before();
  //Step 3
  obj.wait();
  //Step 4
  after();
}

I believe you are not unfamiliar with the above code. We abstract the conditional queue of JDK into four steps and elaborate them one by one

  • Step 1:synchronized (obj)
    • In JDK, if you want to callObject.wait()Method, you must first get thesynchronizedLock, the current step. If the lock is successfully obtained, then it will enter “step 2”. If there is concurrency, the current thread will enter blocking(The thread state is blocked)Until the lock is acquired
  • Step 2:before()
    • We know thatsynchronizedIt is an exclusive lock, so when executing the code in step 2, there is no concurrency in the program, that is, only one thread is executing at the same time, which is relatively easy to understand here
  • Step 3:obj.wait()
    • This step is to put the current thread into the condition queue and release it at the same timeobjSynchronization lock for. It’s against us heresynchronizedIt is generally believed that there is a contradiction between the twosynchronized (obj) {......}The code in the braces will always hold the lock, but the fact is, when the program executeswait()Method, releases theobjSynchronization lock for
  • Step 4:after()
    • Is this step executed concurrently or serially?Suppose we have three threads a, B and C that have been executedwait()Method, and enter the condition queue, waiting for other threads to wake up; Another thread executesnotifyAll()What is the subsequent activation process?
      • Wrong view: many students intuitively feel that threads a, B and C are activated at the same time, so step 4 is executed concurrently; It’s like the 100 meter race. All the students are ready(wait)After a shot(notifyAll)Everyone starts the race and runs to the finish line(Step 4
      • Correct view:In fact, step 4 is executed seriallyAfter you check the code again, you will find that “step 4” is in thesynchronizedBetween the braces of the; Take the above race as an example. If we think that it is “step 4” from hearing the gunshot to running to the finish line, the real scene should be like this: after a gunshot, a starts, B and C stay in place; After a runs to the end, B starts to run, C stays in place; Finally, C ran to the finish line

From this we conclude that,obj.wait()Although it is a native method, it has experienced two steps: releasing the lock and grabbing the lock again

2.2、notify

synchronized (obj) {
  obj.notify();
  // obj.notifyAll();
}

All causesobj.wait()All blocked threads must pass throughnotifyTo wake up

  • notify()In the wake-up condition queue, the first node of the queue
  • notifyAll()Wake up all nodes in condition queue

3、 Conditional queue in AQS (await / signal)

When we first look at the condition queue in AQS, we find that it provides almost the same function as JDK condition queue

JDK AQS
wait await
notify singal
notifyAll singalAll

It is similar in usage

await

//Initialization
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
try {
  lock.lock();
  condition.await();
} catch (InterruptedException e) {
  e.printStackTrace();
} finally {
  lock.unlock();
}

singal

Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
try {
  lock.lock();
  condition.signal();
} finally {
  lock.unlock();
}

3.1. Conditional queue

We know that aBlocking queueThe data structure is as follows:

阻塞队列FIFO数据结构

The image above shows a length of 3 Because the head node is resident in memory, it is not included; We can find that each node in the blocking queue contains pre and post references

That’s another one inside AQSConditional queueWhat kind of data structure is it?

条件队列数据结构

soThe condition queue is a one-way list with only references to the next node; All nodes that are not awakened are stored in the condition queue. The image above shows a length of 5 That is, there are five threads executingawait()method; Unlike the blocking queue, the conditional queue has no “head node” resident in memory and a node in normal statewaitStatusby -2 . When a new node joins, it will be appended to the end of the queue

3.2. Wake up

When we callsignal()Method, what happens? Let’s take a conditional queue of length 5 as an example. In AQS, there will be queue transfer, that is, from conditional queue to blocking queue

signal条件队列向阻塞队列转移

andsignalAll()During the execution, the specific execution process andsignal()Similarly, all nodes in the conditional queue will be transferred to the blocking queue(The concurrency is 1, which is activated in order)In order to activate all threads, the blocking queue wakes up in turn

4、 JDK vs AQS

After the introduction above, it seems that AQS has made a comparison with AQSwait/notifyCompared with the same function, even JDK is more concise; What about their performance? Let’s make a comparison

4.1 comparison

We simulate a scene like thisStart 10 threads and call them respectivelywait()Method to call thenotifyAll()After all 10 threads are waked up and executed, the method ends. The above method is executed 10000 times, and the time consumption of JDK and AQS is compared

JDK test code:

public class ConditionCompareTest {

  @Test
  public void runTest() throws InterruptedException {
    long begin = System.currentTimeMillis();
    for (int i = 0; i < 10000; i++) {
      if (i % 1000 == 0) {
        System.out.println(i);
      }
      jdkTest();
    }
    long cost = System.currentTimeMillis() - begin;
    System. Out. Println ("time consuming: + cost)";
  }
  
  public void jdkTest() throws InterruptedException {
    Object lock = new Object();
    List list = Lists.newArrayList();
    //Step 1: start 10 threads and wait
    for (int i = 0; i < 10; i++) {
      Thread thread = new Thread(() -> {
        try {
          synchronized (lock) {
            lock.wait();
          }
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      });
      thread.start();
      list.add(thread);
    }

    //Step 2: wait for all 10 threads to enter the wait method
    while (true) {
      boolean allWaiting = true;
      for (Thread thread : list) {
        if (thread.getState() != Thread.State.WAITING) {
          allWaiting = false;
          break;
        }
      }
      if (allWaiting) {
        break;
      }
    }

    //Step 3: wake up 10 threads
    synchronized (lock) {
      lock.notifyAll();
    }

    //Step 4: wait for all 10 threads to finish
    for (Thread thread : list) {
      thread.join();
    }
  }
}

AQS test code:

public class ConditionCompareTest {
  private ReentrantLock lock = new ReentrantLock();
  private Condition condition = lock.newCondition();

  @Test
  public void runTest() throws InterruptedException {
    long begin = System.currentTimeMillis();
    for (int i = 0; i < 10000; i++) {
      if (i % 1000 == 0) {
        System.out.println(i);
      }
      aqsTest();
    }
    long cost = System.currentTimeMillis() - begin;
    System. Out. Println ("time consuming: + cost)";
  }

  @Test
  public void aqsTest() throws InterruptedException {
    AtomicInteger lockedNum = new AtomicInteger();
    List list = Lists.newArrayList();
    //Step 1: start 10 threads and wait
    for (int i = 0; i < 10; i++) {
      Thread thread = new Thread(() -> {
        try {
          lock.lock();
          lockedNum.incrementAndGet();
          condition.await();
          lock.unlock();
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      });
      thread.start();
      list.add(thread);
    }

    //Step 2: wait for all 10 threads to enter the wait method
    while (true) {
      if (lockedNum.get() != 10) {
        continue;
      }
      boolean allWaiting = true;
      for (Thread thread : list) {
        if (thread.getState() != Thread.State.WAITING) {
          allWaiting = false;
          break;
        }
      }
      if (allWaiting) {
        break;
      }
    }

    //Step 3: wake up 10 threads
    lock.lock();
    condition.signalAll();
    lock.unlock();

    //Step 4: wait for all 10 threads to finish
    for (Thread thread : list) {
      thread.join();
    }
  }
}
Conditional queue Time consuming 1 Time consuming 2 Time consuming 3 Time consuming 4 It takes 5 minutes Average time consumption (MS)
JDK 5000 5076 5054 5089 4942 5032
AQS 5358 5440 5444 5473 5472 5437

4.2 benchmark Q & A

Based on the above test, we still have some questions. Don’t underestimate these questions. Through these questions, we can connect all the previous knowledge points

  • Q: In AQS testing, “step 2”, why is it necessary to introduce thelockedNum.get() != 10What’s your judgment? Directly determine whether all threads arewaitingIs the method not OK?
  • A: If you really delete itlockedNum.get() != 10In the case of multiple concurrent tests, there is a low probability of program deadlock (the environment of the author’s computer is that there will be one deadlock in 50000 calls on average). Why is there a deadlock? We will find AQS source code, whether it is calledlock()stillawait, the methods used to suspend threads areLockSupport.park()Method, which sets the thread toWAITINGState, that is, thread state, isWAITINGState, it is possible that the thread has just enteredlock()Method, resulting inawaitAndthread.join()Deadlock of
  • Q: If so, why is there no deadlock in JDK testing?
  • A: We can see that JDK is locked throughsynchronizedKeyword, while the thread is waitingsynchronizedThe thread state changes toBLOCKEDAnd enterwait()Method, the state changes toWAITING
  • Q: It seems that only by introducingAtomicInteger lockedNumVariable can solve the deadlock problem
  • A: In fact, there are many ways to solve problems, and we can even simplyReentrantLock lockSetting fair lock can also solve the deadlock problem; Because the current scenario of deadlock is,singalAll()Prior toawait()Occurs when all threads becomeWAITINGThe fair lock ensures that thesingalAll()It must have been called in all threadsawait(). But becausesynchronizedIt is a non fair lock, so if AQS uses fair lock, the performance deviation is large
  • Q: In this way, compared with JDK, the blocking queue in AQS has no advantage. Its usage is not as concise as JDK, and its performance is not as fast
  • A: Indeed, if you only use the blocking and wake-up functions, it is recommended to use the built-in mode of JDK; But the advantage of AQS is not here

5、 AQS conditional queue

The advantage of AQS is that it provides rich API to query the status of condition queue; For example, whenWe want to look at the number of nodes waiting in the condition queue, using JDKwait/notifyIt can’t be done when it’s time; The API provided by AQS is as follows:

  • boolean hasWaiters()Is there a waiting node in the blocking queue
  • int getWaitQueueLength()Gets the length of the blocking queue
  • Collection getWaitingThreads()Gets the thread object in the blocking queue

These APIs provide more flexible control for the program, and the condition queue is not a black box for javaer; Of course, the use of AQS conditional queue must introduce exclusive lock, for exampleReentrantLockNaturally, we can also view some indicators around the condition queue through it, such as:

  • InterruptedIn response to interrupt, exclusive lock provides the ability to respond to interrupt;wait/notifyNo, because althoughwaitMethod response interrupt, butsynchronizedKeywords are always blocked
  • boolean tryLock()Try to acquire the lock;wait/notifyNot available
  • int getHoldCount()Gets the number of blocked threads
  • boolean isLocked()Do you have a lock
  • fair/nonFairProvide fair / unfair lock
  • ...

It can be seen that compared with the whole AQS systemObjectOfwait/notifyThe method is quite flexible and provides a lot of indicators for monitoring condition queue and blocking queue

6、 Thank you

Here is a special thanks to the architect of Shence dataJinmancangI’m also a close friend in private. He has profound skills, and has his own unique insight on the program. During the whole AQS writing period, he took the trouble to provide me with a lot of theoretical and data support, and helped me broaden my horizons. Thank you again!

Recommended Today

Looking for frustration 1.0

I believe you have a basic understanding of trust in yesterday’s article. Today we will give a complete introduction to trust. Why choose rust It’s a language that gives everyone the ability to build reliable and efficient software. You can’t write unsafe code here (unsafe block is not in the scope of discussion). Most of […]