Java线程死锁
2021/5/11 14:25:21
本文主要是介绍Java线程死锁,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
线程死锁
- 什么是死锁
- 一个必然死锁的程序
- 死锁发生的四个必要条件
- 实际生产中死锁问题
- 银行转账问题
- 使获取锁的顺序一致解决银行转账问题
- 哲学家就餐问题以及对应的解决方法
- 解决哲学家就餐问题思路
- 其它线程活跃性问题
- 活锁
- 饥饿
什么是死锁
线程1持有资源A,线程2持有资源B。这时如果线程1去请求资源B,线程2去请求资源A。由于资源A和资源B都已经被其它线程所持有,导致线程1和线程2一直无法获取到想要的资源,陷入无限等待状态。
一个必然死锁的程序
下面通过一段代码来演示下:
/** * 必然死锁的例子 */ public class MustDeadLock implements Runnable { int state = 1; static Object lock1 = new Object(); static Object lock2 = new Object(); public static void main(String[] args) { MustDeadLock mustDeadLock = new MustDeadLock(); MustDeadLock mustDeadLock1 = new MustDeadLock(); mustDeadLock.state = 0; new Thread(mustDeadLock).start(); new Thread(mustDeadLock1).start(); } @Override public void run() { try { if (state == 1) { synchronized (lock1) { System.out.println(Thread.currentThread().getName()+"获取到lock1"); Thread.sleep(500); System.out.println(Thread.currentThread().getName()+"准备获取lock2"); synchronized (lock2) { System.out.println(Thread.currentThread().getName()+"获取到lock2"); } } } if (state == 0) { synchronized (lock2) { System.out.println(Thread.currentThread().getName()+"获取到lock2"); Thread.sleep(500); System.out.println(Thread.currentThread().getName()+"准备获取lock1"); synchronized (lock1) { System.out.println(Thread.currentThread().getName()+"获取到lock1"); } } } } catch (InterruptedException e) { e.printStackTrace(); } } }
执行结果:
Thread-0获取到lock2 Thread-1获取到lock1 Thread-1准备获取lock2 Thread-0准备获取lock1
Thread-0获取到lock2,Thread-1获取到lock1。这时当Thread-1准备获取lock2时,由lock2被Thread-0持有,所以Thread-1就陷入了无限等待状态。Thread-0也一样一直等待获取lock1。
死锁发生的四个必要条件
通过以上例子,我们分析下死锁发生的四个必要条件。
1:互斥条件。无论时lock1 还是lock2,都只能同时被一个线程所持有。
2:持有并请求。Thread-0持有了一个资源lock2,这时它又去请求另外一个资源。
3:不剥夺条件。Thread-0持有的资源lock2只能由它自己去释放,线程不能释放不是由自己持有的资源。
4:环路等待。Thread-0获取到lock2,去请求lock1,Thread-1获取到lock1,去请求lock2。Thread-0在等待Thread-1持有的资源,Thread-1在等待Thread-0持有的资源。Thread-0 --》Thread-1 --》Thread-0。
如果发生死锁,以上这四个条件缺一不可。
实际生产中死锁问题
银行转账问题
有两个用户张三和李四,张三的账户余额有500元,李四的账户余额有500元。在同一时间张三向李四的账户转200元,李四向张三的账户转200元,互相转了100次。以下是具体实现。
/** * 模拟两个人转账导致线程死锁问题 */ public class TransferMoney implements Runnable { Acount a; Acount b; int amount; public TransferMoney(Acount a, Acount b, int amount) { this.a = a; this.b = b; this.amount = amount; } public static void main(String[] args) { Acount acount = new Acount(500); Acount acount1 = new Acount(500); TransferMoney transferMoney = new TransferMoney(acount, acount1, 200); TransferMoney transferMoney2 = new TransferMoney(acount1, acount, 200); for (int i = 0; i < 100; i++) { new Thread(transferMoney).start(); new Thread(transferMoney2).start(); } } @Override public void run() { try { transfer(a, b, amount); } catch (InterruptedException e) { e.printStackTrace(); } } public static void transfer(Acount a, Acount b, int amount) throws InterruptedException { synchronized (a) { synchronized (b) { if (a.money < amount) { System.out.println("账户余额不足,转账失败"); } a.money -= amount; b.money += amount; System.out.println("转账成功"); } } } static class Acount { int money; public Acount(int money) { this.money = money; } } }
执行结果:
互相转账100次,总共应该打印200次转账结果。但只执行了两次程序就没有执行下去了。下面分析下导致这个问题的原因。
张三向李四的转账,必须先获取到张三账户和李四账户两把锁,以防止转账期间其它线程对账户进行操作。如果发生这种情况:一个线程持有了张三账户,另外一个线程持有了李四账户,这时两个线程去请求另一个账户锁的时候就会请求不到。两个线程陷入无限等待状态,同时其它想要获取这两个账户锁的线程也会一直等待。
使获取锁的顺序一致解决银行转账问题
我们可以使获取锁的顺序一致来解决上面银行转账问题。上面问题导致的原因之一是,不同的线程获取锁的顺序不同。张三向李四转账,先获取张三账户锁,再获取李四账户锁;李四向张三转账,先获取李四账户锁,再获取张三账户锁,这样就会形成一个环路:线程1(张三)-》线程2(李四)-》线程1(张三)。所以我们只要破坏这个环路,无论是张三向李四转账还是李四向张三转账,它们获取锁的顺序都是一致的,就可以解决死锁问题。下面是具体实现。
/** * 改变获取锁的顺序解决银行转账死锁问题。 */ public class ChangeOrderDealTransferMoney implements Runnable { TransferMoney.Acount a; TransferMoney.Acount b; int amount; Object lock = new Object(); public ChangeOrderDealTransferMoney(TransferMoney.Acount a, TransferMoney.Acount b, int amount) { this.a = a; this.b = b; this.amount = amount; } public static void main(String[] args) { TransferMoney.Acount acount = new TransferMoney.Acount(500); TransferMoney.Acount acount1 = new TransferMoney.Acount(500); ChangeOrderDealTransferMoney transferMoney = new ChangeOrderDealTransferMoney(acount, acount1, 200); ChangeOrderDealTransferMoney transferMoney2 = new ChangeOrderDealTransferMoney(acount1, acount, 200); for (int i = 0; i < 1000; i++) { new Thread(transferMoney).start(); new Thread(transferMoney2).start(); } } @Override public void run() { // 根据hash值来判断获取锁的顺序,如果有hash冲突,加一个竞争锁。 if (a.hashCode() > b.hashCode()) { synchronized (a) { synchronized (b) { transfer(a, b, amount); } } } if (a.hashCode() < b.hashCode()) { synchronized (b) { synchronized (a) { transfer(a, b, amount); } } } else { synchronized (lock) { transfer(a, b, amount); } } } public static void transfer(TransferMoney.Acount a, TransferMoney.Acount b, int amount) { if (a.money < amount) { System.out.println("账户余额不足,转账失败"); } a.money -= amount; b.money += amount; System.out.println("转账成功"); } }
执行结果:
通过账户的hash值来判断获取锁的顺序,这样不论是张三向李四转账还是李四向张三转账,获取锁的顺序都一致。如果有hash冲突的情况,可以加一个竞争锁来保证线程安全。在实际生产中可以根据索引来判断获取锁的顺序。
哲学家就餐问题以及对应的解决方法
有五位哲学家在一起就餐,每位哲学家左右两边只有一个刀叉。只有拿起刀叉的时候才可以就餐。假设每个哲学家先拿左手边的餐具,再拿右手边的餐具,就完餐后就思考,思考完后又就餐,会发生什么情况。下面用代码演示一下:
/** * 哲学家就餐问题演示。 */ public class DiningPhilosopher implements Runnable { Object knife; Object cross; public DiningPhilosopher(Object knife, Object cross) { this.knife = knife; this.cross = cross; } public static void main(String[] args) { Object[] tableware = new Object[5]; for (int i = 0; i < tableware.length; i++) { tableware[i] = new Object(); } DiningPhilosopher[] diningPhilosophers = new DiningPhilosopher[5]; for (int i = 0; i < diningPhilosophers.length; i++) { diningPhilosophers[i] = new DiningPhilosopher(tableware[i % 4], tableware[(i + 1) % 4]); new Thread(diningPhilosophers[i]).start(); } } @Override public void run() { while (true) { doSomething("thinking"); synchronized (knife) { synchronized (cross) { doSomething("eating"); doSomething("put down cross"); } doSomething("put down knife"); } } } private void doSomething(String str) { System.out.println(Thread.currentThread().getName() + " " + str); } }
执行结果:
可以看到线程陷入了死锁。
解决哲学家就餐问题思路
解决哲学家就餐问题的方法有很多,从破坏死锁的四个必要条件去考虑。
1:从互斥条件考虑。餐具只能被一个人持有,所以这个条件是无法破坏的。
2:持有并请求。哲学家就餐是先拿左手边的餐具,再去拿右手边的餐具。可以让则学家同时去拿左手边的餐具和右手边的餐具。
3:不可剥夺。拿到餐具后,只有自己就餐后才放下。如果有一个服务员,可以提醒哲学家在还未就餐时放下餐具,也可以解决此问题。
4:环路等待。破坏环路等待有两种方式,一是改变一个哲学家拿餐具的顺序。第二:发餐牌,五个人发四张餐牌,只有拿到餐牌的哲学家才能就餐。
以上就是解决则学家就餐问题的一些思路。
其它线程活跃性问题
活锁
线程一直做一些无意义的事情
饥饿
线程一直得不到CPU的调度
这篇关于Java线程死锁的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-26Mybatis官方生成器资料详解与应用教程
- 2024-11-26Mybatis一级缓存资料详解与实战教程
- 2024-11-26Mybatis一级缓存资料详解:新手快速入门
- 2024-11-26SpringBoot3+JDK17搭建后端资料详尽教程
- 2024-11-26Springboot单体架构搭建资料:新手入门教程
- 2024-11-26Springboot单体架构搭建资料详解与实战教程
- 2024-11-26Springboot框架资料:新手入门教程
- 2024-11-26Springboot企业级开发资料入门教程
- 2024-11-26SpringBoot企业级开发资料详解与实战教程
- 2024-11-26Springboot微服务资料:新手入门全攻略