Home  >  Article  >  Java  >  Java multithreading - thread communication examples explained

Java multithreading - thread communication examples explained

高洛峰
高洛峰Original
2017-01-05 15:08:051076browse

The goal of thread communication is to enable threads to send signals to each other. Thread communication, on the other hand, enables threads to wait for signals from other threads.

Communication via shared objects

Busy waiting

wait(), notify() and notifyAll()

Lost signals

False wakeup

Multiple threads waiting for the same signal

Do not call wait() on constant strings or global objects

Communicate through shared objects

A simple way to send signals between threads is to set the signal value in a variable of a shared object. Thread A sets the boolean member variable hasDataToProcess to true in a synchronized block, and thread B also reads the member variable hasDataToProcess in the synchronized block. This simple example uses an object that holds a signal and provides set and check methods:

public class MySignal{
 
 protected boolean hasDataToProcess = false;
 
 public synchronized boolean hasDataToProcess(){
 return this.hasDataToProcess;
 }
 
 public synchronized void setHasDataToProcess(boolean hasData){
 this.hasDataToProcess = hasData;
 }
 
}

Threads A and B must obtain a reference to a shared instance of MySignal in order to communicate. If they hold references to different MySingal instances, then each other will not be able to detect the other's signal. The data that needs to be processed can be stored in a shared cache, which is stored separately from the MySignal instance.

Busy Wait(Busy Wait)

Thread B that is preparing to process data is waiting for the data to become available. In other words, it is waiting for a signal from thread A that causes hasDataToProcess() to return true. Thread B runs in a loop waiting for this signal:

protected MySignal sharedSignal = ...
 
...
 
while(!sharedSignal.hasDataToProcess()){
 //do nothing... busy waiting
}

wait(), notify() and notifyAll()

Busy waiting does not effectively utilize the CPU running the waiting thread , unless the average waiting time is very short. Otherwise, it is wiser to let the waiting thread sleep or be in a non-running state until it receives the signal it is waiting for.

Java has a built-in wait mechanism that allows threads to become non-running while waiting for a signal. The java.lang.Object class defines three methods, wait(), notify() and notifyAll(), to implement this waiting mechanism.

Once a thread calls the wait() method of any object, it will become non-running until another thread calls the notify() method of the same object. In order to call wait() or notify(), the thread must first acquire the lock on that object. In other words, the thread must call wait() or notify() in the synchronized block. The following is a modified version of MySingal - MyWaitNotify using wait() and notify():

public class MonitorObject{
}
 
public class MyWaitNotify{
 
 MonitorObject myMonitorObject = new MonitorObject();
 
 public void doWait(){
 synchronized(myMonitorObject){
  try{
  myMonitorObject.wait();
  } catch(InterruptedException e){...}
 }
 }
 
 public void doNotify(){
 synchronized(myMonitorObject){
  myMonitorObject.notify();
 }
 }
}

The waiting thread will call doWait(), and the waking thread will call doNotify(). When a thread calls the notify() method of an object, one thread among all threads waiting for the object will be awakened and allowed to execute (note: the thread that will be awakened is random, and you cannot specify which thread to awaken). It also provides a notifyAll() method to wake up all threads waiting for a given object.

As you can see, both the waiting thread and the waking thread call wait() and notify() in the synchronized block. This is mandatory! A thread cannot call wait(), notify() or notifyAll() if it does not hold the object lock. Otherwise, an IllegalMonitorStateException exception will be thrown.

(Note: The JVM is implemented in this way. When you call wait, it must first check whether the current thread is the owner of the lock. If not, it will throw an IllegalMonitorStateExcept.)

However, this How can it be? When the waiting thread is executed in the synchronized block, doesn't it always hold the lock of the monitor object (myMonitor object)? Can't the waiting thread block the waking thread from entering the synchronized block of doNotify()? The answer is: indeed not. Once a thread calls the wait() method, it releases the lock it holds on the monitor object. This will allow other threads to also call wait() or notify().

Once a thread is awakened, it cannot exit the wait() method call immediately until the

public class MyWaitNotify2{
 
 MonitorObject myMonitorObject = new MonitorObject();
 boolean wasSignalled = false;
 
 public void doWait(){
 synchronized(myMonitorObject){
  if(!wasSignalled){
  try{
   myMonitorObject.wait();
   } catch(InterruptedException e){...}
  }
  //clear signal and continue running.
  wasSignalled = false;
 }
 }
 
 public void doNotify(){
 synchronized(myMonitorObject){
  wasSignalled = true;
  myMonitorObject.notify();
 }
 }
}
 
<br>

thread that calls notify() exits its own synchronization block. In other words: the awakened thread must reacquire the lock of the monitor object before it can exit the wait() method call, because the wait method call runs in a synchronized block. If multiple threads are awakened by notifyAll(), then only one thread can exit the wait() method at the same time, because each thread must obtain the lock of the monitor object before exiting wait().

Missed Signals

The notify() and notifyAll() methods will not save the method that called them, because when these two methods are called, there may be no thread in Waiting state. The notification signal is discarded after it expires. Therefore, if a thread calls notify() before the notified thread calls wait(), the waiting thread will miss the signal. This may or may not be an issue. However, in some cases this may cause the waiting thread to wait forever and never wake up because the thread missed the wake-up signal.

In order to avoid losing signals, they must be saved in the signal class. In the MyWaitNotify example, the notification signal should be stored in a member variable of the MyWaitNotify instance. The following is a modified version of MyWaitNotify:

public class MyWaitNotify2{
 
 MonitorObject myMonitorObject = new MonitorObject();
 boolean wasSignalled = false;
 
 public void doWait(){
 synchronized(myMonitorObject){
  if(!wasSignalled){
  try{
   myMonitorObject.wait();
   } catch(InterruptedException e){...}
  }
  //clear signal and continue running.
  wasSignalled = false;
 }
 }
 
 public void doNotify(){
 synchronized(myMonitorObject){
  wasSignalled = true;
  myMonitorObject.notify();
 }
 }
}

留意 doNotify()方法在调用 notify()前把 wasSignalled 变量设为 true。同时,留意 doWait()方法在调用 wait()前会检查 wasSignalled 变量。事实上,如果没有信号在前一次 doWait()调用和这次 doWait()调用之间的时间段里被接收到,它将只调用 wait()。

(校注:为了避免信号丢失, 用一个变量来保存是否被通知过。在 notify 前,设置自己已经被通知过。在 wait 后,设置自己没有被通知过,需要等待通知。)

假唤醒

由于莫名其妙的原因,线程有可能在没有调用过 notify()和 notifyAll()的情况下醒来。这就是所谓的假唤醒(spurious wakeups)。无端端地醒过来了。

如果在 MyWaitNotify2 的 doWait()方法里发生了假唤醒,等待线程即使没有收到正确的信号,也能够执行后续的操作。这可能导致你的应用程序出现严重问题。

为了防止假唤醒,保存信号的成员变量将在一个 while 循环里接受检查,而不是在 if 表达式里。这样的一个 while 循环叫做自旋锁(校注:这种做法要慎重,目前的 JVM 实现自旋会消耗 CPU,如果长时间不调用 doNotify 方法,doWait 方法会一直自旋,CPU 会消耗太大)。被唤醒的线程会自旋直到自旋锁(while 循环)里的条件变为 false。以下 MyWaitNotify2 的修改版本展示了这点:

public class MyWaitNotify3{
 
 MonitorObject myMonitorObject = new MonitorObject();
 boolean wasSignalled = false;
 
 public void doWait(){
 synchronized(myMonitorObject){
  while(!wasSignalled){
  try{
   myMonitorObject.wait();
   } catch(InterruptedException e){...}
  }
  //clear signal and continue running.
  wasSignalled = false;
 }
 }
 
 public void doNotify(){
 synchronized(myMonitorObject){
  wasSignalled = true;
  myMonitorObject.notify();
 }
 }
}

留意 wait()方法是在 while 循环里,而不在 if 表达式里。如果等待线程没有收到信号就唤醒,wasSignalled 变量将变为 false,while 循环会再执行一次,促使醒来的线程回到等待状态。

多个线程等待相同信号

如果你有多个线程在等待,被 notifyAll()唤醒,但只有一个被允许继续执行,使用 while 循环也是个好方法。每次只有一个线程可以获得监视器对象锁,意味着只有一个线程可以退出 wait()调用并清除 wasSignalled 标志(设为 false)。一旦这个线程退出 doWait()的同步块,其他线程退出 wait()调用,并在 while 循环里检查 wasSignalled 变量值。但是,这个标志已经被第一个唤醒的线程清除了,所以其余醒来的线程将回到等待状态,直到下次信号到来。

不要在字符串常量或全局对象中调用 wait()

(校注:本章说的字符串常量指的是值为常量的变量)

本文早期的一个版本在 MyWaitNotify 例子里使用字符串常量(””)作为管程对象。以下是那个例子:

public class MyWaitNotify{
 
 String myMonitorObject = "";
 boolean wasSignalled = false;
 
 public void doWait(){
 synchronized(myMonitorObject){
  while(!wasSignalled){
  try{
   myMonitorObject.wait();
   } catch(InterruptedException e){...}
  }
  //clear signal and continue running.
  wasSignalled = false;
 }
 }
 
 public void doNotify(){
 synchronized(myMonitorObject){
  wasSignalled = true;
  myMonitorObject.notify();
 }
 }
}

在空字符串作为锁的同步块(或者其他常量字符串)里调用 wait()和 notify()产生的问题是,JVM/编译器内部会把常量字符串转换成同一个对象。这意味着,即使你有 2 个不同的 MyWaitNotify 实例,它们都引用了相同的空字符串实例。同时也意味着存在这样的风险:在第一个 MyWaitNotify 实例上调用 doWait()的线程会被在第二个 MyWaitNotify 实例上调用 doNotify()的线程唤醒。这种情况可以画成以下这张图:

java 多线程-线程通信实例讲解

起初这可能不像个大问题。毕竟,如果 doNotify()在第二个 MyWaitNotify 实例上被调用,真正发生的事不外乎线程 A 和 B 被错误的唤醒了 。这个被唤醒的线程(A 或者 B)将在 while 循环里检查信号值,然后回到等待状态,因为 doNotify()并没有在第一个 MyWaitNotify 实例上调用,而这个正是它要等待的实例。这种情况相当于引发了一次假唤醒。线程 A 或者 B 在信号值没有更新的情况下唤醒。但是代码处理了这种情况,所以线程回到了等待状态。记住,即使 4 个线程在相同的共享字符串实例上调用 wait()和 notify(),doWait()和 doNotify()里的信号还会被 2 个 MyWaitNotify 实例分别保存。在 MyWaitNotify1 上的一次 doNotify()调用可能唤醒 MyWaitNotify2 的线程,但是信号值只会保存在 MyWaitNotify1 里。

问题在于,由于 doNotify()仅调用了 notify()而不是 notifyAll(),即使有 4 个线程在相同的字符串(空字符串)实例上等待,只能有一个线程被唤醒。所以,如果线程 A 或 B 被发给 C 或 D 的信号唤醒,它会检查自己的信号值,看看有没有信号被接收到,然后回到等待状态。而 C 和 D 都没被唤醒来检查它们实际上接收到的信号值,这样信号便丢失了。这种情况相当于前面所说的丢失信号的问题。C 和 D 被发送过信号,只是都不能对信号作出回应。

If the doNotify() method calls notifyAll() instead of notify(), all waiting threads will be awakened and check the signal value in turn. Threads A and B will return to the waiting state, but only one thread, C or D, will notice the signal and exit the doWait() method call. The other one in C or D will go back to the wait state because the thread that got the signal cleared the signal value (set it to false) on exiting doWait().

After reading the above paragraph, you might try to use notifyAll() instead of notify(), but this is a bad idea in terms of performance. There is no reason to wake up all threads every time when only one thread can respond to the signal.

So: In the wait()/notify() mechanism, do not use global objects, string constants, etc. Corresponding unique objects should be used. For example, instead of calling wait()/notify() on an empty string, each instance of MyWaitNotify3 has its own monitor object.

The above is the collection of information about Java multi-threading and thread communication. We will continue to add relevant information in the future. Thank you for your support of this site!

For more java multi-threading-thread communication examples and related articles, please pay attention to the PHP Chinese website!


Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn