考虑下列情况,一个同步方法的名为T的线程正在执行,并且他需要访问一个名为R的资源,但该资源不可用.T应该做什么?如果T进入某种形式的轮询循环来等待R,那么与T相关的对象就不能被其他线程访问.这不是一个最优的解决方案,因为他没有充分利用多线程环境的程序设计优势.更好的解决方案是让T暂时放弃对这些对象的控制,以允许其他线程继续运行.当R可用时,通知T,然后T继续执行.这种方法依赖于某种形式的线程通信,即一个线程可以通知另一个线程他被阻塞,而其他线程也可以通知他继续执行.Java使用wait(),notify()和notifyAll()方法支持线程间通信.
wait(),notify()和notifyAll()方法是所有对象的一部分,因为他们是由Object类实现的.这些方法只能在同步环境被调用.他们的用法如下:当一个线程暂时阻塞无法运行时,他调用wait(),这会导致线程睡眠,而对象的监视器会被释放,以允许其他线程使用该对象.过一段时间后,当另一个线程进入同一个监视器,调用notify()或notifyAll()时,睡眠线程被唤醒.
下面是Object定义的不同形式的wait():
final void wait() throws InterruptedException final void wait(long millis) throws InterruptedException final void wait(long millis,int nanos) throws InterruptedException
第一种形式会等待,直到被通知.第二种形式会等待,直到被通知或直到经过以毫秒为单位的指定的周期.第三种形式允许以纳秒为单位指定等待周期.
下面是notify()和notifyAll()的基本形式:
final void notify() final void notifyAll()
对notify()的调用恢复了一个等待线程.对notifyAll()的调用通知所有的线程,具有最高优先级的线程获得对象的访问权.
在研究一个使用wait()的例子之前,有一个要点需要指出.尽管wait()通常会等待直到notify()或notifyAll()被调用,但在极少数情况下,等待线程也可能被伪装的唤醒任务唤醒.导致伪装的唤醒任务的条件很复杂.但是,由于存在这种伪装唤醒的可能性,因此,oracle建议对wait()的调用应该出现在一个循环中,该循环会检查线程等待的条件.下面的例子说明了该技术.
一个使用wait()和notify()的示例:
为理解wait()和notify()的应用以及对他们的需要,我们创建一个程序,通过屏幕上显示单词,tick和tock来模拟钟表声音.为此,我们将创建一个包含方法tick()和tock()的TicoTock类.tick()方法显示单词tick.tock方法显示单词tock.为运行钟表,应创建两个线程,一个调用tick()另一个调用tock().目的就是事两个线程以同一种方式运行,程序的输出显示连续的”tick tock”,即一个tick后面跟一个tock的重复模式.
public class test2 { // @param args public static void main(String args[]) { TickTock tt = new TickTock(); MyThread mt1 = new MyThread("Tick",tt); MyThread mt2 = new MyThread("Tock",tt); System.out.println("start"); try { mt1.thrd.join(); mt2.thrd.join(); }catch(InterruptedException exc) { System.out.println("Main Thread interrupted"); } System.out.println("done"); } } class MyThread implements Runnable { Thread thrd; TickTock ttob; MyThread(String name,TickTock tt) { thrd = new Thread(this,name); ttob = tt; thrd.start(); } public void run() { if(thrd.getName().compareTo("Tick")==0) { for(int i=0;i<5;i++) ttob.tick(true); ttob.tick(false); } else { for(int i=0;i<5;i++) ttob.tock(true); ttob.tock(false); } } } class TickTock { String state; synchronized void tick(boolean running) { if(!running) { state = "ticked"; notify(); return; } System.out.print("Tick "); state="ticked"; notify();//let tock() run try { while(!state.equals("tocked")) wait(); }catch(InterruptedException exc) { System.out.println("Thread interrupted"); } } synchronized void tock(boolean running) { if(!running) { state = "tocked"; notify(); return; } System.out.println("Tock"); state="tocked"; notify(); try { while(!state.equals("ticked")) wait(); }catch(InterruptedException exc) { System.out.println("Thread interrupted"); } } }
运行结果
start Tick Tock Tick Tock Tick Tock Tick Tock Tick Tock done
让我们详细研究一下这个程序.时钟的核心是ticktock类,他包含两个方法:tick()和tock(),这两个方法彼此通信确保”tick tock”能够重复出现.注意state字段,当时钟运行时,state将会包含字符串”ticked”或”tocked”,用来指示时钟的状态.在main()中,创建了一个名为tt的ticktock对象,该对象用来启动两个线程的执行.
这两个线程是基于MyThead类型的对象.MyThead构造函数带有两个实参.第一个是线程的名称,即”Tick”或”Tock”.第二个是对TickTock对象的引用,本例中是tt.在MyThread的run()方法中,如果线程名称是”Tick”,则调用tick()方法.如果线程名称是tock这调用tock()方法.有5次调用传递true作为每个方法的实参,只要传递true,时钟就运行.最后一次调用对每一个方法传递false,终止时钟.
程序最重要的部分在TickTock的tick()和tock()方法中.为方便起见,我们首先研究一下tick()方法,如下所示:
synchronized void tick(boolean running) { if(!running) { state = "ticked"; notify(); return; } System.out.print("Tick "); state="ticked"; notify();//let tock() run try { while(!state.equals("tocked")) wait(); }catch(InterruptedException exc) { System.out.println("Thread interrupted"); } }
首先,注意synchronized修饰了tick().切记,wait()和notify()只应用于同步方法.该方法以检查running形参值开始.该形参用于提供一个明确的时钟停止信号.如果他是false,时钟就停止.如果是这种情况,这将state设为ticked,并调用notify()以使任何等待的线程运行.稍后,我们将返回来介绍这一点.
假设当执行tick()时,时钟正在运行,于是显示单词”tick”并将state设置为”thiked”,然后调用notify().调用notify()允许等待同一个对象的线程运行.接下来,在while循环中调用wait(),这会使tick()挂起,直到另一个线程调用notify().因此,只有另一个线程调用了同一个对象的notify(),循环才会进行迭代.结果,当调用tick()时,他就会显示tick,并让另一个线程运行,然后挂起.
调用wait()的while循环会检查state的值,等待他为tocked,而只有在tock()方法执行后,state才会变为tocked.如前所述,使用while循环检查此条件可以防止伪装的唤醒任务重新启动线程.如果在wait()返回时state不是tocked,这意味着发生了伪装的唤醒操作,此时只是简单的调用了wait().
除了显示的内容为tock并将state设为tocked以外,tock方法与tick是完全一样的.因此,当进入时,他显示tock,调用notify(),然后等待.当把他们作为一对来看待时,调用tik()后只能调用tock(),而调用tock()之后也只能调用tick(),依次类推.因此,这两个方法是相互同步的.
时钟停止时,调用notify()的原因是使最后一个wait()调用成功实现.切记,tick()和tock()都在显示他们的消息之后执行wait()调用.这样,问题就在于当时钟停止时,两个方法中有一个还处于等待状态.因此,需要最后一个notify()调用,以使处于等待站台的方法运行.作为一次试验,删除这个notif()调用,观察会发生什么情况.正如你所见到的,程序将”挂起”,你需要退出程序.出现这种情况的原因在于,当最后一个tock()调用wait()时,没有相应的notify()调用让tock()结束.因此,tock()只能待在原地,永远等待.
在继续讨论之前,如果对时钟的正常运行是否确实需要调用wait()和notify()还存有疑问,那就用下面这个版本的tickTock替换上面程序中的TickTock类,这个版本删除了所有的wait()和notify().
class TickTock { String state; synchronized void tick(boolean running) { if(!running) { state = "ticked"; return; } System.out.print("Tick "); state="ticked"; } synchronized void tock(boolean running) { if(!running) { state = "tocked"; return; } System.out.println("Tock"); state="tocked"; } }
很明显,tick()和tock()方法不再同步.
问:我听说过”死锁”这个术语,他用于运行不当的多线程程序.什么是死锁,如何避免呢?还有一个问题,什么是竞争条件(race condition)?怎么避免?
答:顾名思义,死锁描述的情况是一个线程等待另一个线程来做某事,而后者却又在等待前者.因此,两个线程都被挂起,互相等待,谁也执行不了.这就像两位过于礼貌的谦谦君子,都坚持先让对方通过大门.
避免死锁看似容易,其实不然.例如环状死锁,死锁的原因常常不是通过看一下源代码就能找到的.因为并发执行的线程在运行时相互交互的方式十分复杂.为避免死锁,就需要仔细编程,彻底检查.切记,如果一个多线程程序偶尔挂起,那就可能是死锁的缘故.
当两个(或更多个)线程尝试通过访问共享资源,但是又没有进行合适的同步时,就会发生竞争条件.例如,当一个线程增加变量的当前值时,另一个线程可能在向这个变量写入新值.如果没有同步,变量的新值取决于线程的执行顺序(是第一个线程增加了变量的原始值,还是第二个线程写入了新值?),发生这样的情况,就称这两个线程在互相竞争,其结果取决于哪一个线程先执行.与死锁一样,竞争条件的发生可能不太容易发现.解决办法就是以预防为主:仔细的编程,正确的同步对共享资源的访问.