什么是死锁?怎么样才能预防死锁?如何定位死锁?
什么是死锁:
是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。
举个例子:
A 和 B 去按摩洗脚,都想在洗脚的时候,同时顺便做个头部按摩,13 技师擅长足底按摩,14 擅长头部按摩。
这个时候 A 先抢到 14,B 先抢到 13,两个人都想同时洗脚和头部按摩,于是就互不相让,扬言我死也不让你,这样的话,A 抢到 14,想要 13,B 抢到 13,想要 14,在这个想同时洗脚和头部按摩的事情上 A 和 B 就产生了死锁。
怎么解决这个问题呢?
第一种,假如这个时候,来了个 15,刚好也是擅长头部按摩的,A 又没有两个脑袋,自然就归了 B,于是 B 就美滋滋的洗脚和做头部按摩,剩下 A 在旁边气鼓鼓的,这个时候死锁这种情况就被打破了,不存在了。
第二种,C 出场了,用武力强迫 A 和 B,必须先做洗脚,再头部按摩,这种情况下,A 和 B 谁先抢到 13,谁就可以进行下去,另外一个没抢到的,就等着,这种情况下,也不会产生死锁。
总结一下:
死锁是必然发生在多操作者(M>=2 个)情况下,争夺多个资源(N>=2 个,且 N<=M)才会发生这种情况。很明显,单线程自然不会有死锁,只有 B 一个去,不要 2 个,打十个都没问题;单资源呢?只有 13,A 和 B 也只会产生激烈竞争,打得不可开交,谁抢到就是谁的,但不会产生死锁。同时,死锁还有一个重要的要求,争夺资源的顺序不对,如果争夺资源的顺序是一样的,也不会产生死锁。
死锁的发生下四个必要条件:
理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和解除死锁。
如何有效预防死锁的发生:
只要打破四个必要条件之一就可以了
打破互斥条件:改造独占性资源为虚拟资源,大部分资源已无法改造。
打破不可抢占条件:当一进程占有一独占性资源后又申请一独占性资源而无法满足,则退出原占有的资源。
打破占有且申请条件:采用资源预先分配策略,即进程运行前申请全部资源,满足则运行,不然就等待,这样就不会占有且申请。
打破循环等待条件:实现资源有序分配策略,对所有设备实现分类编号,所有进程只能采用按序号递增的形式申请资源。
避免死锁常见的算法:
有有序资源分配法、银行家算法。
现象、危害和解决
在我们 IT 世界有没有存在死锁的情况,有:数据库里多事务而且要同时操作多个表的情况下。所以数据库设计的时候就考虑到了检测死锁和从死锁中恢复的机制。比如 oracle 提供了检测和处理死锁的语句,而 mysql 也提供了“循环依赖检测的机制”
现象
简单顺序死锁示例:
/** * @ClassName NormalDeadLock * @Description TODO 简单顺序死锁测试 * @Date 2020/5/10 16:07 **/ public class NormalDeadLock { private static Object valueFirst = new Object();//第一个锁 private static Object valueSecond = new Object();//第二个锁 //先拿第一个锁,再拿第二个锁 private static void fisrtToSecond() throws InterruptedException { String threadName = Thread.currentThread().getName(); synchronized (valueFirst) { System.out.println(threadName + " get 1st"); Thread.sleep(100); synchronized (valueSecond) { System.out.println(threadName + " get 2nd"); } } } //先拿第二个锁,再拿第一个锁 private static void SecondToFisrt() throws InterruptedException { String threadName = Thread.currentThread().getName(); synchronized (valueSecond) { System.out.println(threadName + " get 2nd"); Thread.sleep(100); synchronized (valueFirst) { System.out.println(threadName + " get 1st"); } } } private static class TestThread extends Thread { private String name; public TestThread(String name) { this.name = name; } public void run() { Thread.currentThread().setName(name); try { SecondToFisrt(); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) { Thread.currentThread().setName("TestDeadLock"); TestThread testThread = new TestThread("SubTestThread"); testThread.start(); try { fisrtToSecond(); } catch (InterruptedException e) { e.printStackTrace(); } } }
简单顺序死锁解决方案:在锁的时,两个方法都 先拿第一个锁,再拿第二个锁
动态顺序死锁及解决方案代码示例
- 用户账户的实体类
- 银行转账动作接口
- 不安全的转账动作的实现(动态顺序死锁)
- 不会产生死锁的安全转账一(解决动态顺序死锁方案一)
- 不会产生死锁的安全转账二(解决动态顺序死锁方案二)
- main函数测试类
用户账户的实体类代码示例
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * 类说明:用户账户的实体类 */ public class UserAccount { private final String name;//账户名称 private int money;//账户余额 private final Lock lock = new ReentrantLock(); public Lock getLock() { return lock; } public UserAccount(String name, int amount) { this.name = name; this.money = amount; } public String getName() { return name; } public int getAmount() { return money; } @Override public String toString() { return "UserAccount{" + "name='" + name + '\'' + ", money=" + money + '}'; } //转入资金 public void addMoney(int amount) { money = money + amount; } //转出资金 public void flyMoney(int amount) { money = money - amount; } }
银行转账动作接口代码示例
/** * 类说明:银行转账动作接口 */ public interface ITransfer { void transfer(UserAccount from, UserAccount to, int amount) throws InterruptedException; }
不安全的转账动作的实现(动态顺序死锁)代码示例
/** * 类说明:不安全的转账动作的实现(动态顺序死锁) */ public class TrasnferAccount implements ITransfer { /** * @param from--转出对象 * @param to--转入对象 * @param amount--账户余额 * @throws InterruptedException */ @Override public void transfer(UserAccount from, UserAccount to, int amount) throws InterruptedException { synchronized (from) { System.out.println(Thread.currentThread().getName() + " get" + from.getName()); Thread.sleep(100); synchronized (to) { System.out.println(Thread.currentThread().getName() + " get" + to.getName()); from.flyMoney(amount); to.addMoney(amount); } } } }
不会产生死锁的安全转账一(解决动态顺序死锁方案一)代码示例
/** * 类说明:不会产生死锁的安全转账一(解决动态顺序死锁方案一) * 原理: * 通过比较hash值决定线程的执行顺序 * 具体实现思路: * 1. 获取各个对象的hash值 * 2.1 若hash值小则先锁对象一,再锁对象二 * 2.2 若hash值大则先锁对象二,再锁对象一 * 2.3 若hash值相同,则在第三把锁的基础上,再同时加锁(Ps:出现此情况的几率特别小,所以在保证线程的安全基础上,不必太在意这一点性能) */ public class SafeOperate implements ITransfer { private static Object tieLock = new Object();//第三把锁 /** * @param from--转出对象 * @param to--转入对象 * @param amount--账户余额 * @throws InterruptedException */ @Override public void transfer(UserAccount from, UserAccount to, int amount) throws InterruptedException { // 获取对象的hash值 int fromHash = System.identityHashCode(from); int toHash = System.identityHashCode(to); // 通过比较hash值决定线程的执行顺序 if (fromHash < toHash) { synchronized (from) { System.out.println(Thread.currentThread().getName() + " get " + from.getName()); Thread.sleep(100); synchronized (to) { System.out.println(Thread.currentThread().getName() + " get " + to.getName()); from.flyMoney(amount); to.addMoney(amount); System.out.println(from); System.out.println(to); } } } else if (toHash < fromHash) { synchronized (to) { System.out.println(Thread.currentThread().getName() + " get" + to.getName()); Thread.sleep(100); synchronized (from) { System.out.println(Thread.currentThread().getName() + " get" + from.getName()); from.flyMoney(amount); to.addMoney(amount); System.out.println(from); System.out.println(to); } } } else { // 若hash值相同,则在第三把锁的基础上,再同时加锁(Ps:出现此情况的几率特别小,所以在保证线程的安全基础上,不必太在意这一点性能) synchronized (tieLock) { synchronized (from) { synchronized (to) { from.flyMoney(amount); to.addMoney(amount); } } } } } }
不会产生死锁的安全转账二(解决动态顺序死锁方案二)代码示例
import java.util.Random; /** * 类说明:不会产生死锁的安全转账二(解决动态顺序死锁方案二) * 原理: * 获取所有的锁后再继续执行 * 具体实现思路: * 1.若未获取第一把锁则while无限循环 * 2.获取第一把锁后,若无法获取第二把锁则释放第一把锁继续循环 * 3.获取第一把锁,同时获取第二把锁时执行任务 * 4.结束 */ public class SafeOperateToo implements ITransfer { /** * @param from--转出对象 * @param to--转入对象 * @param amount--账户余额 * @throws InterruptedException */ @Override public void transfer(UserAccount from, UserAccount to, int amount) throws InterruptedException { // 随机数:避免活锁 Random r = new Random(); while (true) { if (from.getLock().tryLock()) { System.out.println(Thread.currentThread().getName() + " get" + from.getName()); try { if (to.getLock().tryLock()) { try { System.out.println(Thread.currentThread().getName() + " get" + to.getName()); from.flyMoney(amount); to.addMoney(amount); System.out.println(from); System.out.println(to); // 执行完任务后退出 break; } finally { to.getLock().unlock(); } } } finally { from.getLock().unlock(); } } //错开时间,避免活锁 Thread.sleep(r.nextInt(5)); } } }
main函数测试类代码示例
/** * 类说明:模拟支付公司转账的动作 * 测试类 */ public class PayCompany { /*执行转账动作的线程*/ private static class TransferThread extends Thread { private String name; private UserAccount from; private UserAccount to; private int amount; private ITransfer transfer; public TransferThread(String name, UserAccount from, UserAccount to, int amount, ITransfer transfer) { this.name = name; this.from = from; this.to = to; this.amount = amount; this.transfer = transfer; } public void run() { Thread.currentThread().setName(name); try { transfer.transfer(from, to, amount); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) { PayCompany payCompany = new PayCompany(); // 对象一 UserAccount zhangsan = new UserAccount("zhangsan", 20000); // 对象二 UserAccount lisi = new UserAccount("lisi", 20000); // 线程不安全:TrasnferAccount //ITransfer transfer = new TrasnferAccount(); // 线程安全:SafeOperate //ITransfer transfer = new SafeOperate(); // 线程安全:SafeOperateToo ITransfer transfer = new SafeOperateToo(); // 张三转李四 TransferThread zhangsanToLisi = new TransferThread("zhangsanToLisi", zhangsan, lisi, 2000, transfer); // 李四转张三 TransferThread lisiToZhangsan = new TransferThread("lisiToZhangsan", lisi, zhangsan, 4000, transfer); // 线程开始执行 zhangsanToLisi.start(); lisiToZhangsan.start(); } }
危害
1、线程不工作了,但是整个程序还是活着的
2、没有任何的异常信息可以供我们检查。
3、一旦程序发生了发生了死锁,是没有任何的办法恢复的,只能重启程序,对生产平台的程序来说,这是个很严重的问题。
实际工作中的死锁
时间不定,不是每次必现;一旦出现没有任何异常信息,只知道这个应用的所有业务越来越慢,最后停止服务。。。。
若发生死锁,如何定位死锁?
1.打开cmd
2.cmd切换路径到jdk的bin目录下
3.检查是否有死锁信息,输入:jps -
回车:若是有死锁则会打印出死锁的具体信息,若是没有则会出现以下`数据
4.定位死锁的具体信息,输入:jstack 序号(jstack+空格+死锁线程的Id)
回车:即会打印死锁线程的具体信息
定位死锁示例:
cmd 定位死锁:
若有IDEA,左侧的小照相机图标也可定位死锁:
死锁解决方案
关键是保证拿锁的顺序一致
两种解决方式(参考以上代码↑↑↑)
1、内部通过顺序比较,确定拿锁的顺序;
2、采用尝试拿锁的机制。
其他安全问题
活锁(参考以上代码)
两个线程在尝试拿锁的机制中,发生多个线程之间互相谦让,不断发生同一个线程总是拿到同一把锁,在尝试拿另一把锁时因为拿不到,而将本来已经持有的锁释放的过程。
解决办法:
每个线程休眠随机数,错开拿锁的时间。
线程不工作了,但是整个程序还是活着的。