How is the condition in AQS implemented

Posted by umguy on Wed, 16 Feb 2022 15:12:48 +0100

Function of condition

In fact, there are many usage scenarios of condition, which can be used in concurrent scenarios involving condition judgment, such as:

  • Judge whether the queue is full or empty in the ArrayBlockingQueue of the blocking queue
  • Block and wake up all threads in CyclicBarrier
  • Judgment of blocking access queue data in DelayQueue
  • Condition judgment of awaitTermination method in ThreadPoolExecutor of thread pool

How to use condition?

When using synchronized, we can use wait(), notify(), and notifyAll() methods to schedule threads, while condition provides a similar method: wait(),signal(),signalAll function, and can more finely control the waiting range. As mentioned above, jdk uses a lot of ReentrantLock and condition to achieve thread scheduling

Let's look at the most common use of condition: the model of production and consumer:

public class ConditionTest {

    LinkedList<String> lists = new LinkedList<>();

    Lock lock = new ReentrantLock();

    //Condition judgment of whether the set is full
    Condition fullCondition = lock.newCondition();

    //Condition judgment of whether the set is empty
    Condition emptyCondition = lock.newCondition();

    //producer
    private void product(){
        lock.lock();
        try {
            //Suppose the set size is 10
            while (lists.size() == 10){
                System.out.println("list is full");
                fullCondition.await();
            }
            //Produce a 5-bit random string
            String randomString = getRandomString(5);
            lists.add(randomString);
            System.out.println(String.format("product %s size %d  %s",randomString,lists.size(),Thread.currentThread().getName()));
            //Inform consumers that they can consume
            emptyCondition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    //consumer
    private String consume(){
        lock.lock();
        try{
            while (lists.size() == 0){
                System.out.println("list is empty");
                emptyCondition.await();
            }
            String first = lists.removeFirst();
            //Inform the producer that production is ready
            fullCondition.signalAll();
            return first;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
        return null;
    }
    
    /**
     * Generate random string
     * @param length
     * @return
     */
    public static String getRandomString(int length){
        String str="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        Random random=new Random();
        StringBuffer sb=new StringBuffer();
        for(int i=0;i<length;i++){
            int number=random.nextInt(62);
            sb.append(str.charAt(number));
        }
        return sb.toString();
    }

    public static void main(String[] args) {

        ConditionTest test = new ConditionTest();

        ExecutorService executorService = Executors.newCachedThreadPool();

        //The number of threads controls whether consumption is fast or production is fast
        for(int i = 0;i<2;i++){

            executorService.submit(()->{
                System.out.println(Thread.currentThread().getName());
                while (true){
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    test.product();
                }
            });
        }

        for(int k = 0;k<1;k++){
            executorService.submit(()->{
                System.out.println("cousumestart");
                while (true) {
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    String consume = test.consume();
                    System.out.println("consume " + consume+ " "+Thread.currentThread().getName() );
                }
            });
        }

        //Wait for input and block the main thread without exiting
        try {
            new BufferedReader(new InputStreamReader(System.in)).readLine();
        } catch (IOException e) {
            e.printStackTrace();
        }


    }
//Partial output log
product qeV0r size 7  pool-1-thread-1
product xEUkA size 8  pool-1-thread-2
consume P5Je1 pool-1-thread-3
product rQS1D size 8  pool-1-thread-1
product QcEtf size 9  pool-1-thread-2
consume 2q7Fc pool-1-thread-3
product Z5rBg size 9  pool-1-thread-1
consume UBxBD pool-1-thread-3
product Tr5q2 size 9  pool-1-thread-2
product HXBdE size 10  pool-1-thread-1
list is full
consume aYDNR pool-1-thread-3
product ukjnk size 10  pool-1-thread-2
list is full
consume LBEdA pool-1-thread-3
product iK28H size 10  pool-1-thread-2
list is full
list is full

You can see that there are two producer threads and one consumer thread. The speed of production and consumption is the same. Use thread Sleep control,
The production speed is higher than the consumption speed. When the last set of elements reaches 10, the producer calls fullcondition await(); Blocking, only consumers pass fullcondition after consumption signalAll(); Inform the producer to continue production

Similarly, if the number of consumer threads is added to make the consumption speed faster than that of production, emptycondition will be called when the collection is empty await(); Blocking, the producer calls back after production with emptycondition signalAll(); Inform consumers to continue production

Compared with the wait() and notifyAll() methods of the object, they are judged separately under different conditions, with smaller granularity and more accurate range of wake-up threads

Let's take another example of ArrayBlockingQueue, which blocks the acquisition of queue data for a period of time, and returns NULL if it cannot be obtained:

  public E poll(long timeout, TimeUnit unit) throws InterruptedException {
        long nanos = unit.toNanos(timeout);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0) {
                if (nanos <= 0)
                    return null;
                //notEmpty is a condition from lock new
                nanos = notEmpty.awaitNanos(nanos);
            }
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

There are many usage scenarios for condition. Let's take a look at the implementation principle of condition. First, condition needs to implement the class in AbstractQueuedSynchronizer

Analysis of condition principle

We know that AQS maintains a queue to control the execution of threads, and condition uses another waiting queue to judge conditions. Condition must be used after AQS acquire s the lock and calls condition Await () method will add a node to the condition queue. After calling signal() or signalAll(), move the node out of the condition waiting queue and put it into the lock waiting queue to compete for the lock. After taking the lock, continue to execute the subsequent logic.

Topics: Java data structure Back-end Multithreading