死锁问题
什么是死锁?
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
死锁就是两个或者多个相互竞争资源的线程, 你等我, 我等你, 你不放我也不放, 这就造成了他们之间的互相等待, 导致了 “永久” 阻塞.
死锁的四个必要条件
- 互斥使用: 线程1拿到了锁, 线程2就得进入阻塞状态(锁的基本特性).
- 不可抢占: 线程1拿到锁之后, 必须是线程1主动释放, 不可能线程1还没有释放, 线程2强行获取到锁.
- 请求和保持: 线程1拿到锁A后, 再去获取锁B的时候, A这把锁仍然保持, 不会因为要获取锁B就把A释放了.
- 循环等待: 线程1先获取锁A再获取锁B, 线程2先获取锁B再获取锁A, 线程1在获取锁B的时候等待线程2释放B,同时线程2在获取锁A的时候等待线程1释放A
前三点
synchronized
锁的基本特性, 我们是无法改变的, 循环等待是这四个条件里唯一 一个和代码结构相关的, 是我们可以控制的.常见的死锁及解决
不可重入造成的死锁
同一个线程针对同一个对象, 连续加锁两次, 如果锁不是可重入锁, 就会造成死锁问题.
synchronized
具有可重入性, 而在Java中还有一个ReentrantLock
锁也是可重入锁, 所以说, 在Java程序中, 不会出现这种死锁问题.循环等待
哲学家就餐问题(多个线程多把锁)
分析可知每个哲学家有两种状态:- 思考(相当于线程的阻塞状态)
- 拿起筷子吃面条(相当于线程获取到锁然后执行)
我们可以假设一种极端的情况:同一时刻,所有的哲学家同时拿起右手的筷子,这时哲学家需要拿起左手的筷子才能吃饭,然而没有筷子可拿,都在等左边的哲学家放下筷子,这时这里的筷子就相当于程序中的锁,陷入了互相阻塞等待的状态,也就是典型的循环等待造成的死锁问题。
解决方案:
我们可以给按筷子编号, 哲学家们拿筷子时需要遵守一个规则, 拿筷子需要先拿编号小的, 再拿编号大的, 哲学家2, 3, 4, 5
分别拿起了两手边编号为1, 2, 3, 4
编号较小的筷子, 而1
号哲学家想要拿到编号较小的1
号筷子发现已经被拿走了, 此时就空出了5
号筷子, 这样5
号哲学家就可以拿起5
号筷子去吃饭了, 等5
号哲学家放下筷子后,4
号哲学家就可以拿起4
号筷子去吃饭了, 以此类推…对应到程序中, 这样的做法其实就是在给锁编号, 然后再按照一个规定好的顺序来加锁, 任意线程加多把锁的时候, 都让线程遵守这个顺序, 这样就解决了互相阻塞等待的问题.
两个线程两把锁
两个线程两把锁, t1, t2线程先各自针对锁A, 锁B加锁, 然后再去获取对方的锁, 此时双方就会陷入僵持状态, 造成了死锁问题.
观察下面的代码及执行结果:
这里的代码是为了构造一个死锁的场景, 代码中的sleep是为了确保两个线程先把第一个锁拿到, 因为线程是抢占式执行的, 如果没有sleep的作用, 这里的死锁场景是不容易构造出来的.
public class Test { public static void main(String[] args) { Object A = new Object(); Object B = new Object(); Thread t1 = new Thread(() -> { synchronized (A) { System.out.println(Thread.currentThread().getName()+"获取到了锁A"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } synchronized (B) { System.out.println(Thread.currentThread().getName()+"获取到了锁B"); } } }, "t1"); Thread t2 = new Thread(() -> { synchronized (B) { System.out.println(Thread.currentThread().getName()+"获取到了锁B"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } synchronized (A) { System.out.println(Thread.currentThread().getName()+"获取到了锁A"); } } }, "t2"); t1.start(); t2.start(); } }
t1线程获取到了锁A但并没有获取到锁B, t2线程获取到了锁B但并没有获取到锁A, 也就是说t1和t2两个线程进入了相互阻塞的状态, 线程无法获去到两把锁,。
t1线程此时是处于BLOCKED状态的, 表示获取锁, 获取不到的阻塞状态;
t2线程此时也是处于BLOCKED阻塞状态的;解决方案:
们让t1和t2两个线程都按照相同的顺序来获取锁, 比如这里规定先获取锁A, 再获取锁B, 这样按照相同的顺序去获取锁就避免了循环等待造成的死锁问题, 代码如下:@SuppressWarnings({"all"}) public class Test { public static void main(String[] args) { Object A = new Object(); Object B = new Object(); Thread t1 = new Thread(() -> { synchronized (A) { System.out.println(Thread.currentThread().getName()+"获取到了锁A"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } synchronized (B) { System.out.println(Thread.currentThread().getName()+"获取到了锁B"); } } }, "t1"); Thread t2 = new Thread(() -> { synchronized (A) { System.out.println(Thread.currentThread().getName()+"获取到了锁B"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } synchronized (B) { System.out.println(Thread.currentThread().getName()+"获取到了锁A"); } } }, "t2"); t1.start(); t2.start(); } }
最后的执行结果两个线程都获取到了A,B锁.