synchronized实用之线程通信wait¬ify
一、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监视器对象锁其实都是同一个。这会导致什么问题呢?
假设:
线程A,B操作WaitNotifyTester对象实例WaitNotifyTester1,并且线程A正wait在WaitNotifyTester1的monitorObject对象上。
线程C,D操作WaitNotifyTester对象实例WaitNotifyTester2,并且线程C正wait在WaitNotifyTester2的monitorObject对象上。

浙公网安备 33010602011771号