Communication between Java threads -- wait / notify mechanism


Part of this article is from the art of Java Concurrent Programming


volatile and synchronize keywords

If each running thread only runs in isolation, it will have little effect. If multiple threads can cooperate with each other to complete the work, it will bring greater value

Java supports multiple threads to access an object or its member variables at the same time. Using volatile keyword can ensure the visibility of the modified variable, which means that any modification of the variable by any thread can be immediately perceived by other threads

Synchronize keyword can modify a method or synchronization block. It mainly ensures that multiple threads can only have one thread in the method or synchronization block at the same time. It ensures the visibility and exclusivity of thread access to variables. The essence of the implementation of synchronize keyword is to obtain the monitor of an object, and this acquisition process is exclusive, that is, only one thread can obtain the monitor of the object protected by synchronize at the same time

Any thread that accesses an Object must first have its own monitor. If the acquisition fails, the thread enters the synchronization queue and the thread state changes to BLOCKED. When the precursor (the thread that obtained the lock) accessing the Object releases the lock, the release operation will wake up the thread BLOCKED in the synchronization queue and make it try to obtain the monitor again


Waiting notification mechanism

One thread modifies the value of an object, and the other thread senses the change and then performs corresponding operations. The former is a producer and the latter is a consumer. This communication mode realizes decoupling and is more scalable. In Java, in order to achieve similar functions, we can let the consumer thread continuously loop to check whether the variables meet the expectations, and exit the loop if the conditions are met, so as to complete the work of the consumer

while(value != desire) {
    Thread.sleep(1000);
}
doSomething();

The purpose of sleeping for a period of time is to prevent too fast and ineffective attempts. This implementation method seems to meet the needs, but there are two problems:

  • It is difficult to ensure timeliness

    If you sleep too long, it is difficult to find that the conditions have changed in time

  • Difficult to reduce costs

    If you reduce the sleep time, you will consume more processor resources

Using Java provides a built-in wait notification mechanism, which can well solve the above problems. The relevant methods of wait notification are available for any Java object

Method namedescribe
notify()Notify a thread waiting on the object to return from the wait() method. The premise of return is that the thread obtains the lock of the object
notifyAll()Notifies all threads waiting on the object
wait()The thread calling this method enters the WAITING state and returns only when it is notified by another thread or interrupted. Calling this method will release the lock of the object
wait(long)Timeout wait for a period of time. The parameter time is milliseconds
wait(long, int)For more fine-grained control of timeout time, it can reach nanoseconds

Wait notification mechanism means that one thread A calls the wait() method of object o to enter the waiting state, while another thread B calls the notify() or notifyAll() method of object O. thread A returns from the wait() method of object o after receiving the notification, and then performs subsequent operations. The above two threads interact through object o, and the relationship between wait() and notify/notifyAll() on the object is like A switch signal, which is used to complete the interaction between the waiting party and the notifying party

In the following example, two threads, WaitThread and NotifyThread, are created. The former checks whether the flag value is false. If it meets the requirements, follow-up operations are carried out. Otherwise, wait on the lock. The latter notifies the lock after sleeping for a period of time

public class WaitNotify {

    static boolean flag = true;
    static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread waitThread = new Thread(new Wait(), "WaitThread");
        waitThread.start();
        TimeUnit.SECONDS.sleep(1);
        Thread notifyThread = new Thread(new Notify(), "NotifyThread");
        notifyThread.start();
    }

    static class Wait implements Runnable {

        @Override
        public void run() {
            // Lock, Monitor with lock
            synchronized (lock) {
                // Continue to wait and release the lock at the same time
                while (flag) {
                    try {
                        System.out.println(Thread.currentThread() + "flag is true. wait @ "
                                + new SimpleDateFormat("HH:mm:ss").format(new Date()));
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // finish the work
                System.out.println(Thread.currentThread() + "flag is false. running @ "
                    + new SimpleDateFormat("HH:mm:ss").format(new Date()));
            }
        }
    }

    static class Notify implements Runnable {

        @Override
        public void run() {
            // Lock, Monitor with lock
            synchronized (lock) {
                // Obtain the lock of the lock, and then make a notification. The lock of the lock will not be released during the notification
                // WaitThread cannot return from the wait method until the current thread releases the lock
                System.out.println(Thread.currentThread() + " hold lock. notify @ "
                        + new SimpleDateFormat("HH:mm:ss").format(new Date()));
                lock.notifyAll();
                flag = false;
                SleepUtils.second(5);
            }
            // Lock again
            synchronized (lock) {
                System.out.println(Thread.currentThread() + " hold lock again. sleep @ "
                        + new SimpleDateFormat("HH:mm:ss").format(new Date()));
                SleepUtils.second(5);
            }
        }
    }
}

The operation results are as follows

The order of the third and fourth lines of the above results may be interchanged. The execution process of the code is briefly described below

  1. The WaitThread thread starts first, and the NotifyThread thread starts later. Because there is an operation of sleeping for one second, the WaitThread thread obtains the lock first
  2. The WaitThread thread loop judges whether the conditions are met. If not, it calls to execute lock The wait () method releases the lock on the lock object, enters the waiting queue of the lock object, and enters the waiting state
  3. Since the WaitThread thread releases the lock, NotifyThread obtains the lock on the lock object and executes lock Notifyall() method, but it will not release the lock immediately. It just notifies all threads waiting on the lock that they can compete for the lock (the same is true for notify), and sets the flag to false. The execution of this code ends, and the NotifyThread thread releases the lock. At this time, the WaitThread thread and NotifyThread thread compete for the lock together
  4. No matter who gets the lock first, the WaitThread thread and NotifyThread thread can complete the task successfully

The classical paradigm of waiting notification mechanism

From the content of the previous section, we can extract the classic paradigm of waiting notification mechanism, which is divided into two parts, for the waiting party (consumer) and the Notifying Party (producer)

The waiting party shall follow the following principles:

  • Get lock on object
  • If the conditions are not met, call the wait() method of the object and check the conditions after being notified
  • If the conditions are met, the corresponding logic is executed

The pseudo code is as follows:

synchronized(object) {
	while(Conditions not met) {
    	object.wait();
    }
    Corresponding processing logic
}

The notifying party shall follow the following principles:

  • Get lock on object
  • change a condition
  • Notifies all threads waiting on the object

The pseudo code is as follows:

synchronized(object) {
	change a condition
    object.notifyAll();
}

Thread. Use of join()

If A thread A executes thread Join() statement, which means that the current thread A waits for the thread to terminate before starting from thread Join() returns

In the following example, create 10 threads numbered 0 ~ 9. Each thread calls the join() method of the previous thread, that is, thread 0 ends, thread 1 can return from the join() method, and thread 0 needs to wait for the end of the main thread

public class Join {

    public static void main(String[] args) throws InterruptedException {
        Thread previous = Thread.currentThread();
        for (int i = 0; i < 10; i++) {
            // Each thread has a reference to the previous thread. You need to wait for the previous thread to terminate before you can return from the wait
            Thread thread = new Thread(new Domino(previous), String.valueOf(i));
            thread.start();
            previous = thread;
        }
        TimeUnit.SECONDS.sleep(5);
        System.out.println(Thread.currentThread().getName() + " terminate.");
    }

    static class Domino implements Runnable {

        private Thread thread;

        public Domino(Thread thread) {
            this.thread = thread;
        }

        @Override
        public void run() {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " terminate.");
        }
    }
}

The output is as follows

As can be seen from the above output, each thread waits for the termination of the predecessor thread before returning from the join() method. Here, the wait notification mechanism is involved (wait for the termination of the predecessor thread and receive the termination notification of the predecessor thread)

Thread. The source code of join () is simplified as follows

public final synchronized void join(long millis) throws InterruptedException {
    	// Omit previous code
        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        }
    	// Omit the following code
    }

Assuming that the current thread is the main thread, the main thread calls thread Join() method, the main thread will acquire the lock on the thread object and judge whether the thread is still alive. If it is alive, call the wait method to release the lock and wait. If the thread thread has ended, the thread thread will automatically call its notifyAll() method to notify all threads waiting on its thread object, Then the main thread can continue to perform subsequent operations


Added by masgas on Fri, 18 Feb 2022 11:01:08 +0200