Thread Signaling

The purpose of thread signaling is to enable threads to send signals to each other. Additionally, thread signaling enables threads to wait for signals from other threads. For instance, a thread B might wait for a signal from thread A indicating that data is ready to be processed.
线程通信的目的是让多个线程互相发送信号。同时,线程通信也能让线程等待其他线程的信号。例如,线程B等待线程A发来的信号,这个信号表明线程B需要的顺序已经准备好。

Signaling via Shared Objects

A simple way for threads to send signals to each other is by setting the signal values in some shared object variable. Thread A may set the boolean member variable hasDataToProcess to true from inside a synchronized block, and thread B may read the hasDataToProcess member variable, also inside a synchronized block. Here is a simple example of an object that can hold such a signal, and provide methods to set and check it:
**线程发送信号的一种简单方式是将信号值设置在共享变量上。**线程A在同步块中设置成员变量hasDataToProcess=true,线程B在同步块中读取hasDataToProcess。
既然wait()、notify()在synchronized块中,所以要记得synchronized块特性①可见性(也就是happen-before中的Monitor lock rule) ②临界资源互斥性

public class MySignal{

  protected boolean hasDataToProcess = false;

  public synchronized boolean hasDataToProcess(){
    return this.hasDataToProcess;
  }

  public synchronized void setHasDataToProcess(boolean hasData){
    this.hasDataToProcess = hasData;  
  }

}

Thread A and B must have a reference to a shared MySignal instance for the signaling to work. If thread A and B has references to different MySignal instance, they will not detect each others signals. The data to be processed can be located in a shared buffer separate from the MySignal instance.
线程A和B必须具有对相同共享MySignal实例的引用,信令才能起作用。若是线程A、B是不同实例,则互相不能探测到信号。

Busy Wait

Thread B (which is to process the data) is waiting for data to become available for processing. In other words, it is waiting for a signal from thread A which causes hasDataToProcess() to return true. Here is the loop that thread B is running in, while waiting for this signal:
要处理数据的线程B正在等待数据变得可用于处理。
线程B等待来自线程A的信号,线程A设置hasDataToProcess=true,从而使线程B的sharedSignal.hasDataToProcess()为true.(简单来讲,线程B等待其他线程把hasDataToProcess设置成true)。在等待这个信号时,线程B空转。

protected MySignal sharedSignal = ...

...

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

Notice how the while loop keeps executing until hasDataToProcess() returns true. This is called busy waiting. The thread is busy while waiting.
空转执行,直到hasDataToProcess() =true。这叫忙等,也就是等待时候,这个线程也是忙的状态。

wait(), notify() and notifyAll()

Busy waiting is not a very efficient utilization of the CPU in the computer running the waiting thread, except if the average waiting time is very small. Else, it would be smarter if the waiting thread could somehow sleep or become inactive until it receives the signal it is waiting for.
忙等并不是一个充分利用cpu的有效机制,除非等待时间非常短。更有效的方式是等待线程sleep或inactive,直到接收到一直等待的信号。

Java has a builtin wait mechanism that enable threads to become inactive while waiting for signals. The class java.lang.Object defines three methods, wait(), notify(), and notifyAll(), to facilitate this.
java有内在wait机制,它能够让线程在等待信号时 inactive。 java.lang.Object定义了三种方法: wait(), notify(), and notifyAll()

锁池和等待池

  • 锁池:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。
  • 等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁后,进入到了该对象的等待池中。

notify和notifyAll的区别

  • 如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
  • 当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争
  • 优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。

A thread that calls wait() on any object becomes inactive until another thread calls notify() on that object. In order to call either wait() or notify the calling thread must first obtain the lock on that object. In other words, the calling thread must call wait() or notify() from inside a synchronized block. Here is a modified version of MySignal called MyWaitNotify that uses wait() and notify().
在一个共享对象中,调用wait()的线程会inactive,直到其他线程调用notify()。无论是wait() or notify()必须将这个对象锁住,也就是wait() or notify 必须在synchronized块中。

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 would call doWait(), and the notifying thread would call doNotify(). When a thread calls notify() on an object, one of the threads (waiting on that object )are awakened and allowed to execute. There is also a notifyAll() method that will wake all threads waiting on a given object.
线程共享的对象是myMonitorObject。等待线程调用doWait(),唤醒线程调用doNotify()。线程在一个对象上调用notify() ,等待此对象的其中一个线程会被唤醒,继续执行。notifyAll() 会唤醒给定对象的所有线程。

As you can see both the waiting and notifying thread calls wait() and notify() from within a synchronized block. This is mandatory(强制)! A thread cannot call wait(), notify() or notifyAll() without holding the lock on the object the method is called on. If it does, an IllegalMonitorStateException is thrown.
等待线程和唤醒线程在调用wait() and notify()时,必须放在同步块中,也就是锁上(这是强制的)。若不被同步块包裹,则会抛出IllegalMonitorStateException。

But, how is this possible? Wouldn’t the waiting thread keep the lock on the monitor object (myMonitorObject) as long as it is executing inside a synchronized block? Will the waiting thread not block the notifying thread from ever entering the synchronized block in doNotify()? The answer is no. Once a thread calls wait() it releases the lock it holds on the monitor object. This allows other threads to call wait() or notify() too, since these methods must be called from inside a synchronized block.
但是,这怎么可能呢? 只要监视对象在同步块中执行,等待线程不会一直持有监视对象锁吗? 等待线程不会阻塞通知线程,使其永远不会进入doNotify()中的同步块? 答案是不。 线程调用wait()后,它将释放它对监视对象持有的锁。 这允许其他线程也调用wait()或notify(),因为必须从同步块内部调用这些方法。

Once a thread is awakened it cannot exit the wait() call until the thread calling notify() has left its synchronized block. In other words: The awakened thread must reobtain the lock on the monitor object before it can exit the wait() call, because the wait call is nested inside a synchronized block. If multiple threads are awakened using notifyAll() only one awakened thread at a time can exit the wait() method, since each thread must obtain the lock on the monitor object in turn before exiting wait().
一旦线程被唤醒【线程唤醒,不等于获取到监视器对象锁,可能会执行一些线程可并行执行的操作】,它不会退出wait()直到其他线程调用notify()释放了同步锁。也就是说:被唤醒线程在退出wait()前,必须重新获取监视器锁,这是因为wait()是内置在同步代码块中的。如果需要唤醒多个线程,则使用notifyAll,但只有一个线程会退出wait(),这是因为线程在退出wait()之前,要依次获取监视器对象锁。

Missed Signals丢失信号

The methods notify() and notifyAll() do not save the method calls to them in case no threads are waiting when they are called. The notify signal is then just lost. Therefore, if a thread calls notify() before the thread to signal has called wait(), the signal will be missed by the waiting thread. This may or may not be a problem, but in some cases this may result in the waiting thread waiting forever, never waking up, because the signal to wake up was missed.
万一notify() 、notifyAll()调用时没有线程在等待,方法notify()和notifyAll()不会将方法调用保存到它们中。 然后,notify信号就丢失了。 因此,如果线程在发出信号的线程调用wait()之前调用notify(),则等待线程将丢失该信号。 这可能是问题,也可能不是问题,但是在某些情况下,这可能会导致等待线程永远等待,永远不会唤醒,因为错过了唤醒信号。

To avoid losing signals they should be stored inside the signal class. In the MyWaitNotify example the notify signal should be stored in a member variable inside the MyWaitNotify instance. Here is a modified version of MyWaitNotify that does this:
为了避免丢失信号,必须把它们保存在信号类里。在MyWaitNotify例子中,notify信号在

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();
    }
  }
}

Notice how the doNotify() method now sets the wasSignalled variable to true before calling notify(). Also, notice how the doWait() method now checks the wasSignalled variable before calling wait(). In fact it only calls wait() if no signal was received in between the previous doWait() call and this.
doNotify()方法在调用notify()前设置wasSignalled=true。doWait()方法会在调用wait()前检查wasSignalled,若为true,则直接跳过wait方法,因为已经有notify信号发出,也就是已经等到了可以执行的条件,继续执行即可;如果为false,则说明信号还没有发出,就进入wait方法,进行等待。所以,利用一个boolean变量可以解决通知过早问题。在doWait()方法中,最后wasSignalled = false;的原因:可能有多个线程执行doWait(),若其他线程没有等到信号,还是要等待滴。

这样,即使先执行了doNotify(),也不会让后执行的doWait()等待,因为doNotify()已经将wasSignalled=true,doWait()无需等待。从而避免了一直傻等情况。

Spurious Wakeups虚假唤醒

发生Spurious Wakeups的原因:

  • Sometimes, one might have several threads waiting on the same object. Those threads may be interested in different aspects of that object. Say one thread does a notifyAll() on an object, to notify that there have been changes to one particular aspect. All waiting threads will wake up, but only some of them will be interested in the aspect that has changed; the remainder will have experienced a “spurious wake-up”.
    有多个线程等待相同对象A。但是线程等待的对象的点是不同的。也就是说notifyAll() 唤醒了A,但是这只是满足了部分点。所有等待了线程有可能被唤醒,但只有等待这个点的wait才能被唤醒,否则就是错误的唤醒,执行了错误的代码。
  • For inexplicable reasons it is possible for threads to wake up even if notify() and notifyAll() has not been called. This is known as spurious wakeups. Wakeups without any reason.

If a spurious wakeup occurs in the MyWaitNofity2 class’s doWait() method the waiting thread may continue processing without having received a proper signal to do so! This could cause serious problems in your application.
如果一个线程被虚假唤醒发生在MyWaitNofity2的doWait(),会造成程序没有得到合适的信号就继续执行。

To guard against spurious wakeups the signal member variable is checked inside a while loop instead of inside an if-statement. Such a while loop is also called a spin lock. The thread awakened spins around until the condition in the spin lock (while loop) becomes false. Here is a modified version of MyWaitNotify2 that shows this:
为了避免虚假唤醒情况,要用while循环代替if。这个while循环也叫自旋锁。线程
我们使用一个自旋锁机制,也就是用while循环替代if循环,循环检查这样就可以避免虚假唤醒的情况。
在这里插入图片描述
Spurious Wakeups的一种情况是:

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();
    }
  }
}

Notice how the wait() call is now nested inside a while loop instead of an if-statement. If the waiting thread wakes up without having received a signal, the wasSignalled member will still be false, and the while loop will execute once more, causing the awakened thread to go back to waiting.
wait()在while循环中。若wait()在没有得到信号就唤醒,但因为wasSignalled=false,while循环还是会继续执行,从而造成唤醒的线程继续等待。

Multiple Threads Waiting for the Same Signals

The while loop is also a nice solution if you have multiple threads waiting, which are all awakened using notifyAll(), but only one of them should be allowed to continue. Only one thread at a time will be able to obtain the lock on the monitor object, meaning only one thread can exit the wait() call and clear the wasSignalled flag. Once this thread then exits the synchronized block in the doWait() method, the other threads can exit the wait() call and check the wasSignalled member variable inside the while loop. However, this flag was cleared by the first thread waking up, so the rest of the awakened threads go back to waiting, until the next signal arrives.
如果您有多个等待的线程,而while循环也是一个不错的解决方案,这些线程都使用notifyAll()唤醒了,但是只允许其中一个继续。 一次只能有一个线程能够获得监视对象的锁定,这意味着只有一个线程可以退出wait()调用并清除wasSignalled标志。 一旦该线程退出doWait()方法中的同步块,其他线程便可以退出wait()调用并检查while循环内的wasSignalled成员变量。 但是,第一个线程唤醒会清除此标志,因此其余唤醒的线程将返回等待状态,直到下一个信号到达为止。
在这里插入图片描述
这里有两个同步方法,先假设一个X类的对象x,被三个线程使用,其中两个调用w()方法,另一个调用n()方法。假设T1先进入同步块,然后T2,再T3。T1和T2都进入了同步状态,之后T3调用notifyAll,等待线程全部被唤醒,但是谁先获得锁然后执行呢?不知道,都有可能,图示中只是一种可能。

避免Missed Signals、Spurious Wakeups的实际例子—生产者消费者

In the Java language, a unique monitor is associated with every object that has a synchronized method. The CubbyHole class for the producer/consumer example introduced in the previous section has two synchronized methods: the put method, which is used to change the value in the CubbyHole, and the get method, which is used to retrieve the current value. Thus the system associates a unique monitor with every instance of CubbyHole.
在Java语言中,唯一的监视器与具有同步方法的每个对象相关联。CubbyHole是生产者/消费者例子:put用于放值,get用于取值。
Here’s the source for the CubbyHole object. The bold code elements provide for thread synchronization:

class CubbyHole {
    private int contents;
    private boolean available = false;

    public synchronized int get() {
        while (available == false) {
            try {
                wait();
            } catch (InterruptedException e) {
            }
        }
        available = false;
        notifyAll();
        return contents;
    }

    public synchronized void put(int value) {
        while (available == true) {
            try {
                wait();
            } catch (InterruptedException e) {
            }
        }
        contents = value;
        available = true;
        notifyAll();
    }
}

The CubbyHole has two private variables: contents, which is the current contents of the CubbyHole, and the boolean variable available, which indicates whether the CubbyHole contents can be retrieved. When available is true the Producer has just put a new value in the CubbyHole and the Consumer has not yet consumed it. The Consumer can consume the value in the CubbyHole only when available is true.
CubbyHole有两个变量:content用于放内容,available用于内容是否可取出。当available=true,代表生产者已经放置了内容,消费者还没取(也就是消费者可以取)。消费者可以消费只有在available=true时。
Because CubbyHole has synchronized methods, Java provides a unique monitor for each instance of CubbyHole (including the one shared by the Producer and the Consumer). Whenever control enters a synchronized method, the thread that called the method acquires the monitor for the object whose method has been called. Other threads cannot call a synchronized method on the same object until the monitor is released.
因为CubbyHole有put/get同步方法,所以会在每个实例上有个监视器。线程调用同步方法需要先获取监视器,与此同时,其他线程必须等待,直到监视器释放。

①生产者/消费者例子是如何保证不会发生Missed Signals的呢
避免发生Missed Signals,就要使用个共享变量确定是否通知过。这里用的是available变量。若开始available = false,执行了put方法,则会将available=true,从而在get()中不用再等待信号,直接使用即可。

②生产者/消费者例子是如何保证不会发生Spurious Wakeups的呢?
get()、put()方法中wait均用while(available)包裹。

Don’t call wait() on constant String’s or global objects

An earlier version of this text had an edition of the MyWaitNotify example class which used a constant string ( “” ) as monitor object. Here is how that example looked:
下例中使用String作为监视器对象:

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();
    }
  }
}

The problem with calling wait() and notify() on the empty string, or any other constant string is, that the JVM/Compiler internally translates constant strings into the same object. That means, that even if you have two different MyWaitNotify instances, they both reference the same empty string instance. This also means that threads calling doWait() on the first MyWaitNotify instance risk being awakened by doNotify() calls on the second MyWaitNotify instance.
在空字符串或任何其他常量字符串上调用wait()和notify()的问题是:JVM /编译器在内部将常量字符串转换为同一对象。 这意味着,即使您有两个不同的MyWaitNotify实例,它们都引用相同的空字符串实例。 这也意味着在第一个MyWaitNotify实例上调用doWait()的线程可能会被第二个MyWaitNotify实例上的doNotify()调用唤醒。(因为是相同对象)

下面通过一个例子说明,即使是不同的实例(Test、JavaExample),但里面包含相同的String变量,编译器也会优化,会使其指向相同位置
在这里插入图片描述

The situation is sketched in the diagram below:
在这里插入图片描述
Remember, that even if the 4 threads call wait() and notify() on the same shared string instance, the signals from the doWait() and doNotify() calls are stored individually in the two MyWaitNotify instances. A doNotify() call on the MyWaitNotify 1 may wake threads waiting in MyWaitNotify 2, but the signal will only be stored in MyWaitNotify 1.
请记住,即使4个线程在同一个共享字符串实例上调用wait()和notify(),来自doWait()和doNotify()调用的信号也分别存储在两个MyWaitNotify实例中。 在MyWaitNotify 1上进行doNotify()调用可能会唤醒在MyWaitNotify 2中等待的线程,但是信号只会存储在MyWaitNotify 1中。

At first this may not seem like a big problem. After all, if doNotify() is called on the second MyWaitNotify instance all that can really happen is that Thread A and B are awakened by mistake. This awakened thread (A or B) will check its signal in the while loop, and go back to waiting because doNotify() was not called on the first MyWaitNotify instance, in which they are waiting. This situation is equal to a provoked spurious wakeup. Thread A or B awakens without having been signaled. But the code can handle this, so the threads go back to waiting.
起初,这似乎不是一个大问题。 毕竟,如果MyWaitNotify2调用doNotify(),那么可能线程A和B被错误唤醒。 唤醒的线程(A或B)将在while循环中检查其信号,然后返回等待状态,因为MyWaitNotify1未在doNotify()中将wasSignalled=true。 这种情况等同于引起虚假唤醒。 线程A或B在未发出信号的情况下唤醒。 但是代码可以处理此问题,因此线程可以返回等待状态。

The problem is, that since the doNotify() call only calls notify() and not notifyAll(), only one thread is awakened even if 4 threads are waiting on the same string instance (the empty string). So, if one of the threads A or B is awakened when really the signal was for C or D, the awakened thread (A or B) will check its signal, see that no signal was received, and go back to waiting. Neither C or D wakes up to check the signal they had actually received, so the signal is missed. This situation is equal to the missed signals problem described earlier. C and D were sent a signal but fail to respond to it.
问题在于,由于doNotify()调用仅调用notify()而不是notifyAll(),因此即使有4个线程在同一字符串实例(空字符串)上等待,也仅唤醒了一个线程。 因此,如果实际上是针对C或D的信号唤醒了线程A或B中的一个,则唤醒的线程(A或B)将检查其信号,看到没有收到信号,然后返回等待状态。 C或D都不会醒来以检查他们实际收到的信号,因此该信号会丢失。 这种情况等于前面所述的信号丢失问题。 C和D发送了一个信号,但没有响应。

【也就是C、Dnotify发出了信号,本应C、Dwait接收到信号,但被其他实例的线程A、B接收了信号,但因为wasSignal=false,所以被错误接收到无用的信号,从而C、D错过了信号】

If the doNotify() method had called notifyAll() instead of notify(), all waiting threads had been awakened and checked for signals in turn. Thread A and B would have gone back to waiting, but one of either C or D would have noticed the signal and left the doWait() method call. The other of C and D would go back to waiting, because the thread discovering the signal clears it on the way out of doWait().
如果doNotify()方法已调用notifyAll()而不是notify(),则所有等待线程已被唤醒并依次检查信号。 线程A和B将回到等待状态,但是C或D中的一个将注意到该信号并退出doWait()方法调用。 C和D中的另一个将返回等待状态,因为发现信号的线程会在退出doWait()的过程中将其清除。
【虽然唤醒了所有wait线程,即进入了锁池,但通过竞争只有1个会获取锁,其他依然在锁池待著】

You may be tempted then to always call notifyAll() instead notify(), but this is a bad idea performance wise. There is no reason to wake up all threads waiting when only one of them can respond to the signal.
您可能很想总是调用notifyAll()而不是notify(),但这在性能上是一个坏主意。 当只有一个线程可以响应该信号时,没有理由唤醒所有正在等待的线程。

So: Don’t use global objects, string constants etc. for wait() / notify() mechanisms. Use an object that is unique to the construct using it. For instance, each MyWaitNotify3 (example from earlier sections) instance has its own MonitorObject instance rather than using the empty string for wait() / notify() calls.
因此:不要将全局对象(比如字符串常量)等用于wait()/ notify()机制。应该保证每个实例的监视器对象是唯一。 例如,每个MyWaitNotify3(前面部分中的示例)实例都有唯一的MonitorObject实例,而不是将字符串用于wait()/ notify()调用。
参考:http://journals.ecs.soton.ac.uk/java/tutorial/java/threads/monitors.html
http://tutorials.jenkov.com/java-concurrency/thread-signaling.html
https://blog.csdn.net/jdbdh/article/details/81873026
https://www.iteye.com/topic/574357
https://www.zhihu.com/question/37601861

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章