原因
常见的场景
一个线程一把锁
正常情况下,一个线程只能有一把锁,当这个线程想上第二把锁的时候,要释放第一把锁,才能上第二把锁;
而如果想要同时拥有两把锁,那么就是互相矛盾的。
而 Java 中 synchronized 已经对这个问题进行了优化。
在synchronized
中,有一个变量专门记录了当前锁的数量(暂且用count
来表示),也就是说,当线程上了锁 A 后,并不是真的给这个线程上锁,只是这个记录锁数量的变量count++
,如果再上一把锁 B,那么count++
,如果释放锁 B,那么count--
,当count == 0
的时候,说明这个线程已经释放了最开始上的锁 A 了。
两个线程两把锁
现在有线程 1 、线程 2、锁 A 和锁 B。
如果线程 1 先上了锁 A,正要想上锁 B,而正巧不巧,线程 2 上了锁 B,线程 2 还想上锁 A,此时两个线程就僵持住了,线程 1 在等线程 2 释放锁 B,而线程 2 在等线程 1 释放锁 A。
N 个线程 M 把锁
著名的哲学家吃面条问题。
背景:
- 5 个哲学家(5 个线程)
- 五只筷子(5 把锁,可以理解为资源)
- 面条(一个对象)
- 哲学家拿起筷子就一定要吃到面条才会放下筷子并且无人可以个干涉这个时间是多久
一张桌子上有一碗面条,5 个哲学家围着这张桌子,每个哲学家左手边都有 一 只 筷 子,都会思考问题一段时间后吃一口面条,每当吃面条的时候就拿起左手边和右手边的两只筷子。
正巧不巧,突然,每个哲学家都停止了思考,拿起筷子准备吃面条,然后就尬住了,每个人都想拿左手边和右手边的筷子,但是右手边的筷子都其被他哲学家拿起来了。一直等一直等,谁也吃不了面条…
这就是著名的哲学家吃面条问题,能够很好地解释多线程造成死锁的原因。
根据这几个线程造成死锁的现象,可以发现造成死锁的终极原因还是 环路等待。(计算机可不会谦让,说你先你先,我把这把锁让给你)
至于列出造成死锁原因的必要条件
- 互斥使用:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
当举出 哲学家吃面条 的这个例子,就已经能够把这几点概括清楚了
- 互斥使用(哲学家拿起的筷子,别人不能拿);
- 请求与保存条件(一个哲学家拿起一只筷子,没法吃面条,但是也不愿意放下筷子,并且想要竞争另外一个只筷子);
- 不剥削条件(当这几个哲学家拿起筷子的时候,没吃到面条的时候,谁都无权让哲学家放下筷子);
- 循环条件(5 个哲学家同时准备吃面条,但是都只有一只筷子)。
解决死锁的办法
不要在加锁的代码中尝试获取其他锁
这就意味着代码里同一时刻只能获取一把锁,先把一把锁释放了,再获取另外一把锁,那不啥事儿没有
如果每次都只获取一把锁,并且释放完之后再获取其他锁。
约定按照一定的顺序来加锁
比如现在有
- 锁 1,锁 2 和 锁 3;
- 线程 A,线程 B。
约定先加锁 1,再加锁 2,最后加锁 3。
private static Object locker1 = new Object();
private static Object locker2 = new Object();
private static Object locker3 = new Object();
public static void main(String[] args) {
Thread threadA = new Thread() {
@Override
public void run() {
synchronized (locker1) {
synchronized (locker2) {
synchronized (locker3) {
// doSomething
}
}
}
}
};
Thread threadB = new Thread() {
@Override
public void run() {
synchronized (locker1) {
synchronized (locker2) {
synchronized (locker3) {
// doSomething
}
}
}
}
};
}
假设如果不是按照一定顺序来加锁
- 线程 A 加锁顺序:锁 1 、锁 2、锁 3;
- 线程 B 加锁顺序:锁 3、锁 2 、锁 1。
当线程 A 获取到锁 1,又获取到锁 2 ,准备获取锁 3;
而此时线程 B 获取到锁 3, 正准备获取锁 2,然后。。。发现锁 2 已经被获取了,静静等待锁 2 的释放,但是线程 A 想要释放 锁 2,就必须先获得锁 3,而锁 3 想要释放,必须获得锁 2 和 锁 1。。。恭喜,又循环了。