今天做了一个关于线程通信的虚假唤醒题。做的时候没想到,对此也不是很熟悉,查过资料才知道原因,在此自己归纳总结一遍。
问题: 这段代码大多数情况下运行正常,但是某些情况下会出问题。什么时候会出现什么问题?如何修正?
public class MyStack {
private List<String> list = new ArrayList<String>();
public synchronized void push(String value) {
synchronized (this) {
list.add(value);
notify();
}
}
public synchronized String pop() throws InterruptedException {
synchronized (this) {
if (list.size() <= 0) {
wait();
}
return list.remove(list.size() - 1);
}
}
}
个人觉得这个题难就难在没有给run和main方法,发散思维,就如同我写一个方法,只有一条语句System.out,println("Hello,world!"),然后问你这段代码大多数情况下运行正常,但是某些情况下会出现问题,什么时候会出现问题?如何修改?。。。是否是无稽之谈。如果该面试题给出run和main方法,分分钟就找出来,就算不知道虚假唤醒这么回事,找到问题所在,也能想办法解决。
当然这个题的意义就是了解虚假唤醒,也就是不该唤醒的时候唤醒导致线程不安全了。
根据题意分析:
什么时候会出现什么问题?
假设有三个线程A、B、C, A线程执行push方法,B、C线程执行pop方法
首先B线程先执行pop,list中无数据,所以B线程wait进入阻塞状态,然后A线程执行push增加数据,执行完notify()唤醒线程B并且释放该锁。此时就绪队列中有线程B和线程C。再假设线程C先执行,因为list中有数据,所以跳过if执行remove方法,然后线程B执行,线程B直接在wait的下一行执行,没有再判断list中是否有数据就执行了remove方法,而此时list中没有数据,所以出现了越界异常。
注:同步锁锁住的对象的代码就相当于房子,同步锁就是房门的一把锁,线程C从房门开锁进去再锁上没有任何问题,但是线程B由等待到唤醒就相当于房子里有一个地下通道,那样就算房门有钥匙(就是有同步锁)也阻挡不了线程B进入房内执行后面的代码了。
知道了出现的问题是由于唤醒之后没有在此判断list中是否有数据引起的线程不安全,就是虚假唤醒。下面是解决办法:
如何修改?
1.把pop中的if判断改成while判断或者使用线程安全的数据结构存储数据
既然要在唤醒之后再次判断list中是否有数据,那么就用while来判断,没有数据则等待,反之跳出while循环执行remove方法。
2.synchronized修饰的方法和块重复,去掉锁块
就像之前说的,synchronized就是房门的锁,题中synchronized修饰的方法,锁对象是this,锁块的对象也是this,就像一个房门上了两把相同的锁,多此一举,没有作用。
网友评论