Detailed explanation of producer consumer mode implemented by Java wait() / notify()

Time:2021-9-20

Implementing producer consumer mode with Java wait() / notify()

Multithreading in Java involves inter thread communication. Common thread communication methods, such as shared variables and pipe flow, also involve thread communication to realize the producer consumer mode. However, we use the wait() and notify() methods in Java:

wait(): after the thread entering the critical zone has run to a part, it finds that the resources required for the following tasks are not fully prepared, so it calls the wait() method to let the thread block and wait for resources. At the same time, it releases the lock of the critical zone. At this time, the state of the thread also changes from the runnable state to the waiting state;

notify()After preparing the resources, the thread that prepares the resource calls the notify () method to notify the thread that needs to use resources, and releases the lock of the critical area, and locks the critical area to the thread that uses the resource.

The two methods of wait () and notify () must be invoked in the critical region, that is, in the synchronized synchronization block, otherwise the IllegalMonitorStateException exception will be thrown.

Implementation source code:

Producer thread class:

package threads; 
import java.util.List;
import java.util.UUID; 
public class Producer extends Thread{ 
 private List<String> storage;// Producer warehouse
 public Producer(List<String> storage) {
  this.storage = storage;
 }
 public void run(){
  //The producer produces 1 ~ 100 messages every 1s
  long oldTime = System.currentTimeMillis();
  while(true){
   synchronized(storage){
    if (System.currentTimeMillis() - oldTime >= 1000) {
     oldTime = System.currentTimeMillis();
     int size = (int)(Math.random()*100) + 1;
     for (int i = 0; i < size; i++) {
      String msg = UUID.randomUUID().toString();
      storage.add(msg);
     }
     System. Out. Println ("thread" + this. Getname() + "production message" + size + "pieces");
     storage.notify();
    }
   }
  }
 }
}

Consumer thread class:

package threads; 
import java.util.List; 
public class Consumer extends Thread{ 
 private List<String> storage;// Warehouse
 public Consumer(List<String> storage) {
  this.storage = storage;
 }
 public void run(){
  while(true){
   synchronized(storage){
    //When consumers go to the warehouse to get messages, if they find that the warehouse data is empty, they wait
    if (storage.isEmpty()) {
     try {
      storage.wait();
     } catch (InterruptedException e) {
      e.printStackTrace();
     }
    }
    int size = storage.size();
    for (int i = size - 1; i >= 0; i--) {
     storage.remove(i);
    }
    System. Out. Println ("thread" + this. Getname() + "successful consumption" + size + "messages");
   }
  }
 }
}

Warehouse type:

package threads; 
import java.util.ArrayList;
import java.util.List; 

public class Storage { 
 private List<String> storage;// Warehouse shared by producers and consumers
 public Storage() {
  storage = new ArrayList<String>();
 }
 public List<String> getStorage() {
  return storage;
 }
 public void setStorage(List<String> storage) {
  this.storage = storage;
 } 
}

Main method class:


package threads; 
public class App { 
 public static void main(String[] args) {
  Storage storage = new Storage();
  Producer producer = new Producer(storage.getStorage());
  Consumer consumer = new Consumer(storage.getStorage());
  producer.start();
  consumer.start();
 }
}

Production and consumption effect:

Analysis of wait / notify notification mechanism

preface

We know that Java’s wait / notify notification mechanism can be used to realize inter thread communication. Wait indicates the waiting of a thread. Calling this method will cause the thread to block until another thread calls the notify or notifyAll method. The classic producer and consumer model is completed by using the wait / notify mechanism. In this article, we will deeply analyze this mechanism and understand the principle behind it.

Status of the thread

Before understanding the wait / notify mechanism, familiarize yourself with several life cycles of Java threads. They are initial (New), running (runnable), blocked (blocked), waiting (waiting), timed_waiting, terminated and other states (in the java.lang.thread.state enumeration class).

The following is a brief description of these statuses. See the notes for details.

Status name explain
NEW In the initial state, the thread is built, but the start () method is not called
RUNNABLE Running status, after calling the start () method. In Java threads, the ready and running of operating system threads are collectively referred to as running state
BLOCKED In the blocking state, the thread waits to enter the synchronized code block or method and wait to obtain the lock
WAITING In the wait state, a thread can call wait, join and other operations to put itself into the wait state, and wait for other threads to make specific operations (such as notify or interrupt)
TIMED_WAITING Timeout waiting. The thread calls sleep (timeout), wait (timeout) and other operations to enter the timeout waiting state, and returns automatically after timeout
TERMINATED Termination status, thread running ends

We need to know the status and transformation relationship between the above threads

  • Waiting and timed_ Waiting (timeout) will make the thread enter the waiting state. The difference is timed_ Waiting will return automatically after timeout, while waiting needs to wait until the condition changes.
  • The only prerequisite for entering the blocking state is waiting for the synchronization lock to be obtained. The Java annotation makes it clear that there are only two situations that can make a thread enter the blocking state: one is to wait for entering the synchronized block or method, and the other is to re-enter the synchronized block or method after calling the wait () method. This is explained in detail below.
  • The lock implementation of the lock class will not make the thread enter the blocking state. The underlying lock calls the locksupport. Park() method to make the thread enter the waiting state.

Wait / notify use case

Let’s start with an example

The wait () method can make the thread enter the waiting state, while notify () can wake up the waiting state. Such a synchronization mechanism is very suitable for the producer consumer model: consumers consume a resource, while producers produce the resource. When the resource is missing, the consumer calls the wait () method to block itself and wait for the producer’s production; After the production is completed, notify/notifyAll () is invoked to wake consumers to spend.

The following is a code example, where the flag flag indicates the presence or absence of resources.

public class ThreadTest {
    static final Object obj = new Object();
    private static boolean flag = false;
    public static void main(String[] args) throws Exception {
        Thread consume = new Thread(new Consume(), "Consume");
        Thread produce = new Thread(new Produce(), "Produce");
        consume.start();
        Thread.sleep(1000);
        produce.start();
        try {
            produce.join();
            consume.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    //Producer thread
    static class Produce implements Runnable {
        @Override
        public void run() {
            synchronized (obj) {
                System. Out. Println ("enter producer thread");
                System.out.println ("production");
                try {
                    TimeUnit.MILLISECONDS.sleep(2000);  // Simulation of production process
                    flag = true;
                    obj.notify();  // Inform consumers
                    TimeUnit.MILLISECONDS.sleep(1000);  // Simulate other time-consuming operations
                    System. Out. Println ("exit producer thread");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    //Consumer thread
    static class Consume implements Runnable {
        @Override
        public void run() {
            synchronized (obj) {
                System. Out. Println ("enter consumer thread");
                System.out.println("wait flag 1:" + flag);
                While (! Flag) {// judge whether the conditions are met. If not, wait
                    try {
                        System.out.println ("not produced yet, enter and wait");
                        obj.wait();
                        System. Out. Println ("end waiting");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("wait flag 2:" + flag);
                System.out.println ("consumption");
                System. Out. Println ("exit consumer thread");
            }
        }
    }
}

The output result is:

Enter consumer thread

wait flag 1:false

It hasn’t been produced yet. It’s waiting

Enter producer thread

production

Exit producer thread

End waiting

wait flag 2:true

consumption

Exit consumer thread

If you understand the order of output results, you will understand the basic usage of wait / notify. There are the following points to know:

  • It is not reflected in the example, but it is important that the call of the wait / notify method must be in the lock of the object (monitor), that is, when calling these methods, you first need to obtain the lock of the object. Otherwise, the illegalmonitorstateexception will be crawled out.
  • From the output results, after the producer calls notify (), the consumer is not awakened immediately, but wakes up execution after the producer exits the synchronization block. (in fact, it’s easy to understand that the synchronized synchronization method (block) allows only one thread in it at the same time, and the producer does not exit and the consumer cannot enter.)
  • Note that when the consumer is awakened, it is executed after the wait () method (where it is blocked), rather than starting from the synchronization block again.

Deepen understanding

In this section, we explore the relationship between wait / notify and thread state. Gain insight into the life cycle of threads.

It can be seen from the state transition diagram of the previous thread that when the wait () method is called, the thread will enter the waiting state. After being notified (), it will not be executed immediately, but enter the blocking queue waiting to obtain the lock.

Each object has its own waiting queue and blocking queue. Take the producers and consumers in front as an example. We use obj objects as object locks to match the diagram. The internal process is as follows

  • When thread a (consumer) calls the wait () method, thread a gives up the lock, enters the waiting state, and joins the waiting queue of the lock object.
  • The thread B (producer) gets the lock and calls the notify method to notify the waiting queue of the lock object, so that thread A enters the blocking queue from the waiting queue.
  • After thread a enters the blocking queue and thread B releases the lock, thread a competes for the lock and continues to execute from the wait() method.

The above is my personal experience. I hope I can give you a reference, and I hope you can support developpaer.