死锁
可重入性
首先我们先回忆一下什么是可重入性
- 同一个线程可以多次获取同一个锁
- 同一个线程可以获取多个不同的锁
因此获取锁时不但会判断是否是第一次获取,还要记录这是第几次获取。
每获取一次,记录+1,每退出被锁住的代码块一次,记录-1,当记录减到0时锁才真正的释放。
死锁
当两个线程各自持有不同的锁,然后各自试图获取对方手里的锁,造成双方无线等待,这就是死锁
下面我们看个例子
public void add(int m) {
synchronized(lockA) { // 获得lockA的锁
this.value += m;
synchronized(lockB) { // 获得lockB的锁
this.another += m;
} // 释放lockB的锁
} // 释放lockA的锁
}
public void dec(int m) {
synchronized(lockB) { // 获得lockB的锁
this.another -= m;
synchronized(lockA) { // 获得lockA的锁
this.value -= m;
} // 释放lockA的锁
} // 释放lockB的锁
}
当线程1、2分别执行add()和dec()时,如果
- 线程1: 进入add(),获取锁A
- 线程2: 进入dec(),获取锁B
随后 - 线程1: 准备获得锁B,失败,进入等待
- 线程2: 准备获取锁A,失败,进入等待
此时线程1,线程2都陷入了无限的等待中,也就造成了死锁。
死锁发生后,一般只能强制结束JVM线程。
避免死锁的发生
在编写多线程应用时,尽量让获取锁的顺序一致。
wait和notify
前面的synchronized
解决了多线程竞争的问题,但是synchronized
并没有解决多线程协调的问题。以下面的程序为例
class TaskQueue {
Queue<String> queue = new LinkedList<>();
public synchronized void addTask(String s) {
this.queue.add(s);
}
public synchronized String getTask() {
while (queue.isEmpty()) {
}
return queue.remove();
}
}
看上去这个代码没有问题,getTask()
先判断队列是否为空,如果为空则循环等待,直到另一个线程中使用了addTask()
添加了元素,然后就可以正常获取队列中的元素。
但是事实上while
循环永远都不会退出,因为线程在执行while
循环时已经在getTask()
获取了this的锁,其他线程因为没法获得this锁,根本无法调用addTask()
方法。因此while
循环会变成死循环。
我们想要实现的效果是:
- 线程1可以调用
addTask()
方法不断往队列中添加元素 - 线程2可以调用
getTask()
方法获取队列中的元素,如果队列为空,则应该等待,直至队列中有一个元素在返回
由此可以看出,多线程协调运行的原则:当条件不满足时线程进入等待;当条件满足时,线程被唤醒,继续执行
通过wait()方法实现条件不满足时等待:
public synchronized String getTask() {
while (queue.isEmpty()) {
this.wait();
}
return queue.remove();
}
当执行到while循环中的wait()
方法时,wait()
不会返回且线程将等待状态,直到被其他进程唤醒wait()
才会返回,然后继续执行下一条语句。
wait()方法的执行机制非常复杂,我们只需大概了解。首先,它不是一个普通的Java方法,而是定义在Object类的一个native方法,也就是由JVM的C代码实现的。其次,必须在synchronized块中才能调用wait()方法,因为wait()方法调用时,会释放线程获得的锁,wait()方法返回后,线程又会重新试图获得锁。
需要注意的是只能在锁对象上调用wait()
方法
如getTask()
的锁对象是this
,所以在this
上调用wait()
通过notify()实现条件满足时唤醒线程:
public synchronized void addTask(String s) {
this.queue.add(s);
this.notify(); // 唤醒在this锁等待的线程
}
notify()
方法会唤醒一个正在this
锁等待的线程,从而使得等待的线程的wait()
方法返回
接下来我们看一个完整的例子
public class Main {
public static void main(String[] args) throws InterruptedException {
var q = new TaskQueue();
var ts = new ArrayList<Thread>();
for (int i=0; i<5; i++) {
var t = new Thread() {
public void run() {
// 执行task:
while (true) {
try {
String s = q.getTask();
System.out.println("execute task: " + s);
} catch (InterruptedException e) {
return;
}
}
}
};
t.start();
ts.add(t);
}
var add = new Thread(() -> {
for (int i=0; i<10; i++) {
// 放入task:
String s = "t-" + Math.random();
System.out.println("add task: " + s);
q.addTask(s);
try { Thread.sleep(100); } catch(InterruptedException e) {}
}
});
add.start();
add.join();
Thread.sleep(100);
for (var t : ts) {
t.interrupt();
}
}
}
class TaskQueue {
Queue<String> queue = new LinkedList<>();
public synchronized void addTask(String s) {
this.queue.add(s);
this.notifyAll();
}
public synchronized String getTask() throws InterruptedException {
while (queue.isEmpty()) {
this.wait();
}
return queue.remove();
}
}
这个例子中,addTask()
方法,内部调用了notifyAll()
方法而不是notify()
,使用notifyAll()
将唤醒所有正在当前this锁等待的线程,而notify()
只会唤醒一个(具体哪个依赖操作系统)。这是因为可能有多个线程在getTask()
内部等待。通常来说使用notifyAll()
更安全,因为如果我们的代码考虑不全,使用notify()
很有可能导致一些线程一睡不醒。
特别需要注意的是,wait()方法返回时需要重新获得锁,所以必须等待唤醒这个线程的线程结束,且即使使用notifyAll()
一次也只能有一个线程能获得锁,其余继续等待
另外在上述例子中,wait()
方法只能用在while循环中,如果是if判断,wait()返回后会直接进入下一条语句,但此时队列仍有可能为空.