今天看到一个笔试题,刚开始竟然有点蒙。经过一番摸索之后才发现问题所在,特此记录下来。
1.面试题
下面的代码在绝大部分时间内都运行得很正常,请问在什么情况下会出现问题?问题的根源在哪里?
import java.util.LinkedList;
public class Stack {
LinkedList list = new LinkedList();
public synchronized void push(Object x) {
synchronized(list) {
list.addLast( x );
notify();
}
}
public synchronized Object pop()
throws Exception {
synchronized(list) {
if( list.size() <= 0 ) {
wait();
}
return list.removeLast();
}
}
}
猛地一看,这是不是脑子有坑啊?加这么多重量级锁。没办法,咱管不了,只能分析这个代码会出现什么问题。
2.分析
每个方法都加了一把锁,方法内还对list这个对象加了锁。如果按照正常的push、pop顺序来执行的话也没啥问题。那就从list的边界值开始测试吧。在没有值的情况下,先pop,再push。设计如下代码:
public static void main(String[] args) throws InterruptedException {
Stack myStack = new Stack();
Thread t1 = new Thread(() -> {
try {
myStack.pop();
} catch (Exception e) {
e.printStackTrace();
}
});
t1.start();
//确保t1线程首先执行
Thread.sleep(1000);
myStack.push(1);
}
当断点跟着以上测试代码运行的时候,直接测出问题了:push()
方法中的list.addLast( x );
这一行根本进不去,也就是说代码停在了synchronized(list)
这一行,在该代码块中获取list
对象的锁时失败了。
为什么获取list
对象的锁失败呢?因为pop()
方法中的list
锁没被释放。为什么没被释放呢?接下来得看看wait()
这个方法,jdk8
中的解释如下:
- wait()
Causes the current thread to wait until another thread invokes the notify() method or the notifyAll() method for this object.
翻译过来就是:使得当前的线程等待直到另一个线程对该对象调用了notify()或者notifyAll()方法.
- notify()
方法notify()
的解释如下:
Wakes up a single thread that is waiting on this object's monitor.
翻译过来就是:唤醒一个在当前对象的monitor中进行等待的线程
另外我们需要知道wait()
方法是必须跟synchronized
进行配套使用,否则会出现IllegalMonitorStateException
的异常。而synchronized
不同的使用方式,也需要wait()
使用不同的方式进行等待。
-
synchronized
锁定在普通方法上
这个相当于锁定了该方法所在类的对象实例上,在该方法中使用wait()
或者this.wait()
均可进行释放锁和线程暂停。 -
synchronized
锁定在静态方法上
这个相当于锁定了该方法所在的类,在该方法中需要使用类名.class.wait()
(在本例中即为Stack.class.wait())的方式来释放锁进行线程暂停。 -
synchronized
锁定在指定对象上
这个相当于锁定了指定对象,需要使用对象.wait()
(在本例中即为list.wait())来释放锁并暂停线程。
当我们在pop()
方法中首先把list
进行了锁定,然后直接调用wait()
方法时,表示的是调用Stack
这个类的实例对象所在的线程进行了等待,也就是测试代码中t1
线程阻塞住了,但并没有把对象list
解锁。
当1秒后执行到main
线程中的myStack.push(1);
这一行代码时,push()
方法中首先尝试锁定list
,但由于被t1
线程给锁定后并没有释放,导致这里获取锁失败,故而卡在这里动不了,最终造成了死锁。
3.解决方法
既然找到了原因,那就好说了。pop()
和push()
方法及方法体中均有两层锁,我们需要去掉一层。
- 方法1: 去掉方法上的
synchronized
锁
public class Stack {
LinkedList list = new LinkedList();
public void push(Object x) {
synchronized(list) {
list.addLast( x );
list.notify();
}
}
public Object pop() throws Exception {
synchronized(list) {
if ( list.size() <= 0 ) {
list.wait();
}
return list.removeLast();
}
}
}
- 方法2: 去掉
list
对象上的锁
public class Stack {
LinkedList list = new LinkedList();
public synchronized void push(Object x) {
list.addLast(x);
this.notify();
}
public synchronized Object pop() throws Exception {
if (list.size() <= 0) {
this.wait();
}
return list.removeLast();
}
}