java concurrency <信号量>
使用信号量的目的
线程信号量的目的是使线程能够彼此发送信号。 此外,线程信号量使线程能够等待来自其他线程的信号。 例如,线程B可能等待来自线程A的信号,指示数据准备好被处理。
Java 通过共享对象实现信号量
线程相互发送信号的一种简单方式是通过将信号值设置在某些共享对象变量中。 线程A可以将布尔成员变量hasDataToProcess从同步块内部设置为true,线程B可以读取hasDataToProcess成员变量,也可以在同步块内部。 以下是可以容纳这种信号的对象的简单示例,并提供设置和检查它的方法:
public class MySignal{
protected boolean hasDataToProcess = false;
public synchronized boolean hasDataToProcess(){
return this.hasDataToProcess;
}
public synchronized void setHasDataToProcess(boolean hasData){
this.hasDataToProcess = hasData;
}
}
线程A和B必须具有对共享的MySignal实例的引用,以使信号量工作。 如果线程A和B引用了不同的MySignal实例,则它们将不会检测到彼此的信号。 要处理的数据可以位于与MySignal实例分开的共享缓冲区中。
Busy Wait
处理数据的线程B正在等待数据可用于处理。 换句话说,它正在等待来自线程A的信号,这会导致hasDataToProcess()返回true。 这是线程B正在运行的循环,同时等待这个信号:
下面模拟信号量主线程在果断的改变信号量,线程myThread11在繁忙的等待着信号的变化。
public class SignalMain {
public static void main(String[] args) throws InterruptedException {
MySignal mySignal = new MySignal();
Thread myThread11 = new Thread(new MyThread1(mySignal));
myThread11.start();
Thread.sleep(5000);
mySignal.setHasDataToProcess(true);
Thread.sleep(5000);
mySignal.setHasDataToProcess(false);
}
static class MyThread1 implements Runnable{
private MySignal mySignal;
public MyThread1(MySignal mySignal) {
this.mySignal = mySignal;
}
@Override
public void run() {
while(true){
try {
while(mySignal.isHasDataToProcess()){
Thread.currentThread().sleep(2000);
System.err.println("i am running");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
wait(), notify() and notifyAll()
繁忙的等待不是在运行等待线程的计算机中CPU的非常有效的利用,除非平均等待时间非常小。 否则,如果等待的线程可能会以某种方式睡眠或变得不活动,直到它接收到正在等待的信号,这将更加明智。
Java有一个内置的等待机制,使得线程在等待信号时变得不活动。 java.lang.Object类定义了wait(),notify()和notifyAll()这三个方法,以便于此。
在任何对象上调用wait()的线程变为无效,直到另一个线程调用该对象上的notify()。 为了调用wait()或通知调用线程必须先获取该对象的锁。 换句话说,调用线程必须从同步块内部调用wait()或notify()。 这是MySignal的一个修改版本MyWaitNotify,它使用wait()和notify()。
使用对象的的三个方法作为信号量
public class MyWaitNotify {
MonitorObject myMonitorObject = new MonitorObject();
public void doWait(){
synchronized(myMonitorObject){
try{
myMonitorObject.wait();
} catch(InterruptedException e){
e.printStackTrace();
}
}
}
public void doNotify(){
synchronized(myMonitorObject){
myMonitorObject.notify();
}
}
}
模拟测试demo
public class MyWaitNotifyMain {
public static void main(String[] args) throws InterruptedException {
MyWaitNotify myWaitNotify = new MyWaitNotify();
Thread myThread11 = new Thread(new MyWaitNotifyMain.MyThread1(myWaitNotify));
myThread11.start();
Thread.sleep(5000);
myWaitNotify.doNotify();
Thread.sleep(5000);
myWaitNotify.doNotify();
}
static class MyThread1 implements Runnable {
private MyWaitNotify myWaitNotify;
public MyThread1(MyWaitNotify myWaitNotify) {
this.myWaitNotify = myWaitNotify;
}
@Override
public void run() {
while (true) {
try {
myWaitNotify.doWait();
Thread.currentThread().sleep(2000);
System.err.println("i am running");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
等待的线程将调用doWait(),通知线程将调用doNotify()。当线程在对象上调用notify()时,等待该对象的其中一个线程被唤醒并被允许执行。还有一个notifyAll()方法将唤醒等待给定对象的所有线程。
正如您可以看到等待和通知线程在同步块内调用wait()和notify()。这是强制性的线程不能调用wait(),notify()或notifyAll(),而不需要在调用该方法的对象上锁定该锁。如果是,则抛出IllegalMonitorStateException。
但是,这是怎么可能的?只要在同步块内执行,等待线程不会保持监视对象(myMonitorObject)上的锁定?等待线程是否阻止通知线程从doNotify()中进入同步块?答案是不。一旦一个线程调用wait()它释放它在监视器对象上的锁定。这允许其他线程调用wait()或notify(),因为这些方法必须从同步块内部调用。
一旦线程被唤醒,它不能退出wait()调用,直到线程调用notify()已经离开其同步块。换句话说:唤醒的线程必须重新获取监视对象上的锁,才能退出wait()调用,因为等待调用嵌套在同步块中。如果使用notifyAll()唤醒多个线程,则一次只能有一个唤醒的线程可以退出wait()方法,因为每个线程必须在退出wait()之前依次获取监视对象的锁定。
丢失的信号
方法notify()和notifyAll()不保存对它们的方法调用,以防线程在被调用时等待。 然后通知信号就会丢失。 因此,如果一个线程在线程之前调用notify(),信号已经被调用了wait(),信号将被等待的线程错过。 这可能是或可能不是一个问题,但在某些情况下,这可能导致等待线程永远等待,从不醒来,因为醒来的信号被错过了。
为了避免丢失信号,它们应该存储在信号类中。 在MyWaitNotify示例中,通知信号应存储在MyWaitNotify实例中的成员变量中。 以下是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();
}
}
}
注意,在调用notify()之前,doNotify()方法现在将wasSignalled变量设置为true。 另外,请注意doWait()方法现在在调用wait()之前检查wasSignalled变量。 实际上,如果在之前的doWait()调用之间没有收到信号,它只会调用wait()。
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语句中。 如果等待的线程在没有收到信号的情况下唤醒,则isSignalled成员仍然是false,while循环将再次执行,导致唤醒的线程返回等待。
等待同一信号的多线程
while循环也是一个很好的解决方案,如果你有多个线程等待,这些都使用notifyAll()唤醒,但只允许其中一个继续。 一次只能有一个线程能够获取监视器对象上的锁定,这意味着只有一个线程可以退出wait()调用并清除wasSignalled标志。 一旦该线程在doWait()方法中退出同步块,其他线程就可以退出wait()调用,并检查while循环中的isSignalled成员变量。 然而,这个标志被第一个线程清除,所以其余的唤醒的线程返回等待,直到下一个信号到达。
不要在常量String或全局对象上调用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 / Compiler将常量字符串转换为同一个对象。 这意味着,即使您有两个不同的MyWaitNotify实例,它们都引用同一个空字符串实例。 这也意味着第一个MyWaitNotify实例上调用doWait()的线程有可能被第二个MyWaitNotify实例上的doNotify()调用唤醒。
情况如下图所示:
image记住,即使4个线程在同一个共享字符串实例上调用wait()和notify(),doWait()和doNotify()调用的信号也分别存储在两个MyWaitNotify实例中。 MyWaitNotify 1上的doNotify()调用可能唤醒在MyWaitNotify 2中等待的线程,但信号将仅存储在MyWaitNotify 1中。
起初这可能不是一个大问题。毕竟,如果在第二个MyWaitNotify实例上调用了doNotify(),那么真正可以发生的是,线程A和B被错误地唤醒。这个唤醒的线程(A或B)将在while循环中检查它的信号,并返回到等待,因为doNotify()没有在第一个MyWaitNotify实例上被调用,他们正在等待。这种情况相当于一个诱发的虚假唤醒。线程A或B唤醒没有发出信号。但代码可以处理这个,所以线程返回等待。
问题是,由于doNotify()调用只调用notify()而不是notifyAll(),即使有4个线程正在等待同一个字符串实例(空字符串),也只有一个线程被唤醒。因此,如果线程A或B中的一个在真正的信号为C或D时被唤醒,则唤醒的线程(A或B)将检查其信号,看到没有收到信号,并返回等待。 C或D都不醒来检查他们实际收到的信号,所以信号被错过。这种情况等于前面提到的错过的信号问题。 C和D发送信号,但没有响应。
如果doNotify()方法调用了notifyAll()而不是notify(),则所有等待的线程都被唤醒并依次检查信号。线程A和B将返回等待,但C或D中的一个将注意到该信号,并留下了doWait()方法调用。 C和D中的另一个将返回等待,因为发现信号的线程会在doWait()的出路上清除。
你可能会被诱惑,总是调用notifyAll()而通知(),但这是一个坏主意性能明智。没有理由唤醒所有的线程等待,只有其中一个可以响应信号。
所以:不要对wait()/ notify()机制使用全局对象,字符串常量等。使用使用它的构造是唯一的对象。例如,每个MyWaitNotify3(早期部分的示例)实例都有自己的MonitorObject实例,而不是为wait()/ notify()调用使用空字符串
网友评论