Java并发编程3-等待、通知和中断

来源:互联网 发布:动态视频制作软件 编辑:程序博客网 时间:2024/05/29 15:28

J:hi,T。
T:hi,J。
J:今天打算给大家介绍点什么呢?
T:今天我打算介绍一下Java并发编程中的等待、通知和中断机制。
J:哦,听起来好高深。
T:呵呵,我先做一些简单介绍吧。在Java中,每一个对象都有一个等待集合,当线程执行对象的wait操作时,线程将被放入到对象的等待集合中;当线程执行对象的notify操作时,对象等待集合中的线程将被唤醒;当线程的interrupt操作被调用时,线程将从等待状态中被唤醒,并抛出中断异常。
J:听起来有点复杂。
T:是的,看看下面这个例子会不会好一点:

public class test {private List<Object> list = new ArrayList<Object>();public synchronized void add(Object obj) {list.add(obj);//通知唤醒处理线程notifyAll();}public synchronized Object get() {while (list.size() == 0) {try {//等待添加线程唤醒wait();} catch (InterruptedException e) {e.printStackTrace();}}return list.remove(0);}}


这是一个简单的阻塞队列(没有做长度限制),线程调用add方法向队列中添加数据,其它线程通过get方法从队列中获取数据处理,如果队列中没有数据了,get的线程就会阻塞等待添加线程唤醒,添加线程添加数据后调用notifyAll唤醒处理线程处理。
J:我大概明白了,但还是有很多疑问。
T:能说说吗?
J:比如:我还不明白wait和notifyAll都做了些什么,而且wait操作为什么要放到一个while循环里面,你的例子里面也没有包含到中断操作,对吧?
T:是的,你说的这些问题我都会在后面解答,在开始之前,我们需要先了解等待集合。

等待集合

Java中的每一个对象,除了有一个关联的监视器外,还有一个关联的等待集合,该集合用于存放对应该对象的等待线程,从等待集合添加线程和移除线程的操作都是原子的。
当一个对象刚创建的时候,它的等待集合是空的,当线程调用wait操作时,线程被添加到等待集合中,当通过notify和notifyAll唤醒线程后,线程从等待集合中移除,当线程发生中断时,线程也会从等待集合中移除。
J:我理解等待集合也就是等待线程的一个管理集合,是吧?
T:非常正确,我们接着看等待操作。

等待

在Java中提供了3个方法可以导致线程等待wait(),wait(long millisecs)和wait(long millisecs,int nanosecs)(祥见JDK文档说明)。我们假定线程t在对象m上执行了wait方法,并且t在m上有n个锁,那么:
1)如果n是0(即t没有获取m的监视器的锁),抛出IllegalMonitorStateException异常;
2)如果wait带有参数,且nanosecs参数不在返回0-999999内,或者millisecs参数是负数,那么一个IllegalArgumentException异常将抛出;
3)如果线程t被中断,一个InterruptedException异常将抛出,t的中断状态被设置到false(注:当线程t发生中断时,t的中断状态将被设置为true,即调用t.isInterrupted()后将返回true,但当InterruptedException异常抛出后,t的中断状态将被设置为false,注意调用t.isInterrupted()也会导致t的中断状态设置为false);
4)否则,将执行下面的步骤:
 (1)线程t被增加到对象m的等待集合中,并执行对应的n个解锁行为;
 (2)线程t等待,直到从对象m的等待集合中移除。下面的行为都会导致线程t从等待集合中移除:
  ------对象m上的notify操作被执行;
  ------对象m上的notifyAll操作被执行;
  ------线程t的interrupt操作被执行;
  ------wait的时间到达;
  ------出现了“虚假唤醒”(一个线程在没有收到唤醒通知的情况下被唤醒,就成为“虚假唤醒”,这么做是允许的,但是不推荐)。“虚假唤醒”有可能会重复的出现,这就导致了你应该把wait操作放到一个循环中,每次唤醒后判断条件是否满足,如果不满足,则继续等待。
 (3)线程t在对象m上执行n个锁操作;
 (4)如果线程t是通过中断从对象m的等待集合中移除的,wait方法将抛出InterruptedException异常,并且将t的中断状态设置到false。
J:我明白了,wait操作将释放线程在对象上的锁,所以在开始的例子中get线程在wait后,add线程才能够获取到test对象的锁继续执行。而wait的线程唤醒后,也需要先获取到监视器的锁才能继续执行,是吧?
T:是的。
J:而且wait操作放在一个while循环里面也是因为“虚假唤醒”,是吧?
T:不仅仅是这样,实际上存在两个原因:
1)如果有多个线程在同一个对象的等待队列中,另一个线程调用了该对象的notifyAll方法后,所有的线程都将唤醒,但可能你的线程并不能满足条件,需要判断如果不满足条件,就继续等待,notifyAll也可能会多次调用,因此将wait操作放在while循环中;
2)由于“虚假唤醒”的存在。
J:我明白了。
T:我们来做个小测试巩固一下吧,看看下面这段代码会得到怎样的打印输出:

public class test {private List<Object> list = new ArrayList<Object>();public synchronized void add(Object obj) {list.add(obj);notifyAll();try {wait(10000);} catch (InterruptedException e) {e.printStackTrace();}}public synchronized Object get() {while (list.size() == 0) {try {wait();} catch (InterruptedException e) {e.printStackTrace();}}return list.remove(0);}public static void main(String[] args) {final test1 t1 = new test1();Thread t = new Thread(new Runnable() {@Overridepublic void run() {t1.add("dddd");}});t.start();try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(t1.getT() + " " + System.currentTimeMillis());System.out.println(t1.get() + " " + System.currentTimeMillis());}}class test1{private test t = new test();public synchronized void add(Object obj){t.add(obj);}public synchronized int get(){return 100;}public Object getT(){return t.get();}}


J(一段时间之后):我在我的电脑上执行了一下,得到了这样的结果:
dddd 1395988509025
100 1395988518025
但我不知道为什么?
T:让我来解释一下吧。该程序的执行步骤如下:
1)线程t启动,调用t1.add("dddd"),在test类的add方法中进入wait 10秒的操作;
2)主线程sleep 1秒后开始调用t1.getT(),getT方法没有同步,直接进入test的get方法,由于线程1执行了wait,释放了锁,所以主线程能顺利进入,调用成功,打印dddd 1395981888418;
3)主线程继续向下执行,调用t1.get方法,主线程需要获取t1的锁,但t1的锁已经被线程t获取,因此主线程等待;
4)线程t等待10秒后恢复执行,结束;
5)主线程获取到t1的做,继续执行,打印100 1395981897419,这个打印的时间之刚好和先前打印的时间值大约相差9秒(线程t wait的10秒减去主线程中sleep的1秒,考虑到还有其它因素,可能会有毫秒级的误差)。
J:好复杂。
T:是的,这里也向我们展示了一个线程在多个对象上加锁存在的潜在风险,可能会导致严重的性能问题,甚至死锁,因此我们应该尽量保持锁的简单。
J:是的,简单是非常重要的。
T:好了,我们继续。

通知

当调用对象的notify和notifyAll的时候,就会触发通知行为。假定线程t是在对象m上调用通知方法的线程,并且t在m上有n个锁,下面的情况将会出现:
1)如果n是0,抛出IllegalMonitorStateException异常;
2)如果n大于0,且执行的是notify操作,如果m的等待集合不是空的,则选择一个线程u从等待集合中移除,线程u的选择是任意的,没有确定的顺序。线程u从等待集合中移除后,将等待t释放了在m上的所有锁,然后从wait中恢复执行;
3)如果n大于0,并且执行了notifyAll操作,所有等待集合中的线程都将被移除。但需要注意的是,一次只能有一个线程能够获取到监视器的锁,其它线程将会等待。
思考下这个问题呢:如果你想在一个线程中唤醒另一个线程,最可靠的方式是什么?
J:哦,我想应该使用notifyAll吧。
T:是的,notify只会任意唤醒一个线程,并不能保证一定会唤醒你的线程。我们继续。

中断

当调用Thread.interrupt时,就会引发一个线程中断。
假定t是调用u.interrupt的线程,u是另一个线程,那么u的中断状态被设置到true。如果线程u在对象m的等待集合中,则u将从m的等待集合中移除,u将从wait行为中恢复,即重新获取m的监视器的锁,然后抛出InterruptedException异常。
可以通过Thread.isInterrupted判断线程的中断状态,也可以通过Thread.interrupted查看和清除线程的中断状态。
J:我现在知道了通知和中断都会起到唤醒线程的作用,那么如果你个线程在收到通知被唤醒的同时又被中断了,那会产生什么情况呢?
T:这个问题非常好,下面我将回答这个问题。

等待、通知和中断的相互作用

如果线程处于等待状态中,当notify和interrupt同时发生时,这时可能发生的情况如下:
1)从wait正常返回,但任然存在一个悬置的中断(换句话说,一个对Thread.interrupted的调用将返回true),或者
2)从wait返回,并抛出InterruptedException异常。
通知不会由于中断而丢失。假定对象m有一个等待集合,另一个线程在m上执行了notify操作,那么或者
1)至少一个m的等待集合中的线程将从wait中正常返回,或者
2)在m的等待集合中的所有线程都抛出InterruptedException,退出wait。
需要注意的是如果一个线程同时收到中断和notify唤醒,并且该线程最终通过抛出InterruptedException异常唤醒,那么等待集合中将会有另一个线程被nofity唤醒。
J:今天的内容好复杂,我们讲了等待、通知和中断操作的核心“等待集合”,讲解了等待、通知和中断操作执行的详细步骤,以及他们的相互作用。
T:希望你能有所收获。
J:我得好好消化一下了。
T:好的,那么,下次再见了。
J:再见。

0 0
原创粉丝点击