synchronized实用之线程通信wait&notify

一、Wait/Notify线程通信机制原理

线程通信的目标是使线程间能够互相发送信号。另一方面,线程通信使线程能够等待其他线程的信号。例如,线程B可以等待线程A的一个信号,这个信号会通知线程B数据已经准备好了。

 

Java有一个内建的线程通信机制就是使用wait()notify()notifyAll()。这种通信机制也是synchronized锁机制的实际运用,因为线程必须在同步块里调用wait()或者notify()。

 

一个线程一旦调用了任意对象的wait()方法,它就释放了所持有的监视器对象上的锁,并转为非运行状态。这将允许其他线程也可以调用 wait()或者notify()。 而一旦另一个线程持有了相同的锁之后,通过调用同一个对象的notify()方法将可以随机唤醒一个等待在同一个对象上的阻塞线程。notifyAll()则可以唤醒所有等待在同一个对象上的阻塞线程。

 

一旦一个线程被唤醒,必须等到调用了notify或notifyAll方法的线程退出它自己的同步块释放锁之后,并重新获得监视器对象的锁,才可以退出wait()的方法调用,继续执行wait()代码行后面的代码。如果多个线程被notifyAll()唤醒,那么在同一时刻 将只有一个线程可以退出wait()方法,因为每个线程在退出wait()前必须获得监视器对象的锁。

 二、线程通信时要注意的问题

        1. 唤醒信号丢失

          如果一个线程先于被通知线程调用wait()前调用了notify(),等待的线程将错过这个信号。在某些 情况下,这可能使等待线程永远在等待,不再醒来,因为线程错过了唤醒信号。

          为了避免丢失信号,必须把它们保存在信号类里。示例:

 1 public class WaitNotifyTester{  
 2    
 3   MonitorObject monitorObject = new MonitorObject();  
 4   boolean wasSignalled = false;  
 5    
 6   public void doWait(){  
 7     synchronized(monitorObject){  
 8       if(!wasSignalled){  
 9         try{  
10           monitorObject.wait();  
11          } catch(InterruptedException e){...}  
12       }  
13       //clear signal and continue running.  
14       wasSignalled = false;  
15     }  
16   }  
17    
18   public void doNotify(){  
19     synchronized(monitorObject){  
20       wasSignalled = true;  
21       monitorObject.notify();  
22     }  
23   }  
24 }  

以上示例中,为了避免唤醒信号丢失, 用一个变量wasSignalled来保存是否被通知过。在notify前,设置自己已经被通知过。在wait前,先检测是否被通知过再决定是否等待通知。在wait后,设置自己没有被通知过,需要等待通知。

       2. 无效的唤醒

           有时候由于莫名其妙的原因,线程有可能在没有调用过notify()和notifyAll()的情况下醒来。这也就是所谓的假唤醒(spurious wakeups)。这可能会在某些时候导致你的应用程序出现严重问题。

           为了防止这种无效的假唤醒,可以通过自旋锁检测通知信号,而不是if表达式。诚然,自旋锁对CPU的消耗非常大,慎用之。

 1 public class WaitNotifyTester{  
 2    
 3   MonitorObject monitorObject = new MonitorObject();  
 4   boolean wasSignalled = false;  
 5    
 6   public void doWait(){  
 7     synchronized(monitorObject){  
 8       while(!wasSignalled){  
 9         try{  
10           monitorObject.wait();  
11          } catch(InterruptedException e){...}  
12       }  
13       //clear signal and continue running.  
14       wasSignalled = false;  
15     }  
16   }  
17    
18   public void doNotify(){  
19     synchronized(monitorObject){  
20       wasSignalled = true;  
21       monitorObject.notify();  
22     }  
23   }  
24 }  

通过以上示例中将检测通知信号放在while循环里面,一旦发生假唤醒之后,可以再次判断是否条件满足,如果没有满足则继续调用wait()等待。

 

3. 多个线程被同时唤醒

          如果你有多个线程在等待,被notifyAll()全部唤醒,但只允许一个线程能继续执行,以上while示例也可以解决这种要求:每次只有一个线程能够获得监视器monitorObject对象锁,意味着只有一个线程可以退出wait()调用并清除wasSignalled标志(设为false),而当接下来其他线程进入同步块之后,由于wasSignalled标识已经被第一个线程清除,所以全部继续调用wait回到等待状态,直到下次信号到来。

        4.不要使用字符串常量或全局对象调用wait()

         以下是以空字符串作为锁的同步块的示例:

 1 public class WaitNotifyTester{  
 2    
 3   String monitorObject = "";  
 4   boolean wasSignalled = false;  
 5    
 6   public void doWait(){  
 7     synchronized(monitorObject){  
 8       while(!wasSignalled){  
 9         try{  
10           monitorObject.wait();  
11          } catch(InterruptedException e){...}  
12       }  
13       //clear signal and continue running.  
14       wasSignalled = false;  
15     }  
16   }  
17    
18   public void doNotify(){  
19     synchronized(monitorObject){  
20       wasSignalled = true;  
21       monitorObject.notify();  
22     }  
23   }  
24 }  

由于字符串常量对象指向的内存地址都是常量池中相同的一块地址,所以就算有两个以上WaitNotifyTester对象实例,其内部的monitorObject监视器对象锁其实都是同一个。这会导致什么问题呢?

假设:

线程AB操作WaitNotifyTester对象实例WaitNotifyTester1,并且线程A正wait在WaitNotifyTester1的monitorObject对象上。 

线程CD操作WaitNotifyTester对象实例WaitNotifyTester2,并且线程C正wait在WaitNotifyTester2的monitorObject对象上。

按照设想,可以通过B唤醒A,通过D唤醒C。但是由于使用了字符串常量作为monitorObject,所以他们持有的其实是相同的锁。也就是说,当B线程执行notify时,可能唤醒的不是A线程而是C线程。这时候,C线程醒来之后,发现是假唤醒(因为wasSignalled保存的是特定WaitNotifyTester实例对象的信号),能够继续等待,这没有问题,但是A线程却错过了对唤醒信号的响应而继续处于等待状态。
如何解决这种情况?
1. 可能我们首先想到的则是将notify()换成notifyAll(),但是这在性能上是个糟糕的解决办法,因为唤醒了不必要的线程,并且这些无效线程在唤醒和继续等待这种线程上下文切换中会消耗大量的CPU资源。
2.  最好的解决办法是不要使用全局对象,字符串常量等。应该使用对应唯一的对象作为监视器对象锁。
 
posted @ 2021-05-11 15:14  莫待樱开春来踏雪觅芳踪  阅读(461)  评论(0)    收藏  举报