2023.10.17 Use of wait and notify and understanding CountDownLatch

Table of Contents

Introduction

Usage of method

Introducing an instance (wait version without parameters)

wait method execution process

wait and notify combined example

wait version with parameters

The difference between notify and notifyAll

Classic examples

Summary

CountDownLatch


Introduction

  • The biggest problem with threads is preemptive execution and random scheduling
  • Although thread scheduling in the kernel is random, some APIs can be used to control the execution order between threads, allowing threads to actively block and give up the CPU so that other threads can use it

Simple example

  • There are threads t1 and t2. I hope that thread t1 will work first, and when it is almost done, let thread t2 do the work
  • At this time, you can let thread t2 wait (block, actively give up the CPU), wait until thread t1 is almost done, and then notify thread t2 through notify, wake up thread t2, and let thread t2 continue to work
Note:

Differences in usage between join or sleep and wait and notify

  • Using join means that thread t1 must be completely executed before thread t2 can run. If you want thread t1 to do half of the work first, and then let thread t2 continue to do it, join will not be able to meet the demand
  • Use sleep to specify a sleep time. Similarly, it is difficult for us to know the specific time required for thread t1 to do half of the work, so it is difficult to meet our needs
  • It can be considered that wait and notify cover the uses of join, but the use of wait and notify is much more troublesome than join. You can choose according to the actual usage scenario

Use of method

Introduction instance (wait version without parameters)

public class ThreadDemo16 {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
// wait does not add any parameters, it means waiting until other threads wake it up.
        object.wait();
    }
}

Execution results:

  • This exception is an illegal lock status exception
  • There are only two lock states: one is the locked state, which is always the unlocked state
  • As to why the exception occurs, we also need to understand the execution process of the wait method

wait method execution process

  • Release the lock first
  • Blocking wait
  • After receiving the notification, try to acquire the lock again, and after acquiring the lock, continue execution

Modify instance

public class ThreadDemo16 {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
// wait does not add any parameters, it means waiting until other threads wake it up.
        synchronized (object) {
            System.out.println("wait before");
            object.wait();
            System.out.println("after wait");
        }
    }
}

Run results:

  • Compared with calling the wait method when the object is not locked, an error will occur when the lock status is abnormal
  • Here, first lock the object, and then call the wait method, you can block and wait well, and be in the WAITING state
  • Blocking here releases the lock of object object, so that other threads can acquire the lock of object object

wait and notify combined instance

package Thread;

public class ThreadDemo17 {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        Thread t1 = new Thread(() -> {
// This thread is responsible for waiting
            System.out.println("t1: wait before");

            synchronized (object) {
                try {
                    object.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t1: after wait");
        });

        Thread t2 = new Thread(() -> {
//This thread is responsible for waking up
            System.out.println("t2: notify before");
            synchronized (object) {
// notify must obtain the lock before notification can be made
                object.notify();
            }
            System.out.println("t2: after notify");
        });

        t1.start();
//Add sleep here to wait for 1 second
// This is to try to ensure that thread t1 is executed first and then thread t2 is executed.
        Thread.sleep(1000);
        t2.start();
    }
}

Run results:

Note:

  • The notify here needs to be paired with wait
  • If the object used by wait is inconsistent with the object used by notify
  • Notify will have no effect at this time
  • Because notify can only wake up threads waiting on the same object

  • Although the code sequence here is to execute thread t1 first and then thread t2
  • However, due to the randomness of thread scheduling, there is no complete guarantee that thread t1 will be executed first and then thread t2
  • If no thread is waiting when notify is called, the wait cannot be awakened at this time, thenthis notification is an invalid notification, but it will not have any side effects
  • So after executing thread t1, sleep and wait for 1 second before executing thread t2. This can ensure that thread t1 executes the wait method first

wait version with parameters

  • The wait in the above code is a parameter-less version, which means that as long as thread t2 does not notify, thread t1 will always wait, that is, waiting to death
  • So the parameterized version of wait can specify a maximum waiting time, which can effectively avoid dead waiting situations

Note:

  • Although the parameterized version of wait looks a bit like sleep
  • You can specify the waiting time
  • Can be woken up in advance, use notify for wait and interrupt for sleep
  • But there are still essential differences, and their meanings are completely different
  • Notify wakes up wait, which is normal business logic and will not cause any abnormalities
  • If interrupt wakes up from sleep, an interrupt exception will be triggered first, indicating that this is a problematic logic

The difference between notify and notifyAll

  • When there are multiple threads waiting for the object object
  • If a thread executes the notify method, a waiting thread will be randomly awakened
  • If a thread executes the notifyAll method, all waiting threads will be awakened, and then these threads will compete for the lock together

Classic examples

  • There are three threads, which can only print A, B, and C respectively. The three threads are controlled to print in the order of ABC

Specific ideas

  • We can create two objects object1 and object2
  • object1 is used to control the execution order of thread t1 and thread t2
  • object2 is used to control the execution order of thread t2 and thread t3
  • Let thread t3 wait block waiting for object object2 until thread t2 executes notify
  • Let thread t2 wait block the waiting object object1 until thread t1 executes notify
  • This will ensure that thread t1 is executed first, then thread t2, and finally thread t3
public class ThreadDemo18 {
    public static void main(String[] args) throws InterruptedException {
        Object object1 = new Object();
        Object object2 = new Object();

        Thread t1 = new Thread(() -> {
            System.out.println("A");
            synchronized (object1) {
                object1.notify();
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (object1) {
                try {
                    object1.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("B");
            synchronized (object2) {
                object2.notify();
            }
        });

        Thread t3 = new Thread(() -> {
            synchronized (object2) {
                try {
                    object2.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("C");
        });

        t2.start();
        t3.start();
        Thread.sleep(500);
        t1.start();
    }
}

Run results:

Note:

  • The reason why we arrange the execution sequence of threads in this way and wait 0.5 seconds before executing thread t1
  • This is because it can largely prevent thread t1 from executing notify before thread t2 blocks the waiting object object1 before executing the wait method.
  • As a result, thread t1 notify becomes lonely, causing thread t2 to be blocked and waiting, resulting in a deadlock

Summary

  • The two APIs of wait and notify are used to control the execution order between threads
  • wait and notify both belong to methods of the Object class

CountDownLatch

Comprehension by analogy

  • There is a running game here

  • The start time of this running game is clear (it starts when the referee fires the starting gun)
  • The end time is unclear (it ends when all runners cross the finish line)
  • In order to wait for the end of this running game, this CountDownLatch is introduced

Two methods

  • await (wait means waiting, a represents all), the main thread calls this method
  • countDown means the runner has crossed the finish line

Specific ideas

  • CountDownLatch specifies a count (here is the number of players) when constructing it
  • For example, four players compete
  • If you call await initially, it will block
  • When every player crosses the finish line, the countDown method will be called
  • For the first three times the countDown method is called, await has no effect
  • The fourth time you call the countDown method, await will be awakened and return (unblocked)
  • At this point it can be considered that the entire game is over

Application scenarios

  • Download a large file, such as Thunder, steam, etc. for multi-thread download
  • Multi-threaded downloading means dividing a large file into multiple small files and arranging multiple threads to download them separately
  • Here you can use CountDownLatch to distinguish whether the entire download has been completed