目录
死锁是指两个或者两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造的一种阻塞的现象,若无外力作用,他们都将无法推进下去。此时称系统处于死锁状态或者系统产生了死锁,这些永远互相等待的进程称为死锁进程。(当然线程也是如此)比如下面的这个表情包就很好地解释了死锁的一种情况。
或者我们也可以用另外一个例子来帮助大家理解死锁:
张三和女神一起去饺子馆吃饺子. 吃饺子需要酱油和醋.
张三抄起了酱油瓶, 女神抄起了醋瓶.
张三: 你先把醋瓶给我, 我用完了就把酱油瓶给你.
女神: 你先把酱油瓶给我, 我用完了就把醋瓶给你.
如果这俩人彼此之间互不相让, 就构成了死锁.
酱油和醋相当于是两把锁, 这两个人就是两个线程
这里大家可以自行去了解一下哲学家就餐问题_百度百科,因为问题比较麻烦,所以不花过多篇幅去讲解了,我们直接抛出结论。
死锁产生的四个必要条件:
互斥使用:即当资源被一个线程使用(占有)时,别的线程不能使用
不可抢占:资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
请求和保持:即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
循环等待:即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样 就形成了一个等待环路。
当上述四个条件都成立的时候,便形成死锁。
当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。 其中最容易破坏的就是 "循环等待"。
破坏循环等待
最常用的一种死锁阻止技术就是锁排序. 假设有 N 个线程尝试获取 M 把锁, 就可以针对 M 把锁进行编号 (1, 2, 3...M). N 个线程尝试获取锁的时候, 都按照固定的按编号由小到大顺序来获取锁. 这样就可以避免环路等待.
可能产生环路等待的代码:
- Object lock1 = new Object();
- Object lock2 = new Object();
- Thread t1 = new Thread() {
- @Override
- public void run() {
- synchronized (lock1) {
- synchronized (lock2) {
- // do something...
- }
- }
- }
- };
- t1.start();
- Thread t2 = new Thread() {
- @Override
- public void run() {
- synchronized (lock2) {
- synchronized (lock1) {
- // do something...
- }
- }
- }
- };
- t2.start();
两个线程对于加锁的顺序没有约定,容易产生环路等待。(注意不是一定产生死锁,只是大概率)
不会产生环路等待的代码:
- Object lock1 = new Object();
- Object lock2 = new Object();
- Thread t1 = new Thread() {
- @Override
- public void run() {
- synchronized (lock1) {
- synchronized (lock2) {
- // do something...
- }
- }
- }
- };
- t1.start();
- Thread t2 = new Thread() {
- @Override
- public void run() {
- synchronized (lock1) {
- synchronized (lock2) {
- // do something...
- }
- }
- }
- };
- t2.start();
约定好先获取 lock1, 再获取 lock2 , 就不会环路等待。
当然预防出现死锁的方法不止一种,我们只是讲了一种比较简单易懂的方法。大家感兴趣的还可以去学习一下银行家算法,它同样可以避免死锁,但比较复杂我们不再展开。