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

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