目录

JAVA中死锁的定义:

死锁实例

1)实例业务场景

解决方案:定义锁的顺序,并且整个应用中都按照这个顺序来获取锁。

2)实例业务场景

解决方案:

总结造成死锁的原因:

如何防患?

定位死锁(解决方案)

心得:


JAVA中死锁的定义:

在JAVA中我们会使用加锁机制来保证线程的安全,但如果过度使用加锁操作,可能会出现死锁的情况。举个例子:当一个线程永远的持有一个锁, 并且其他线程都尝试获取这个锁时,那么它们将永远被阻塞。在线程A持有锁L并想获取锁M的同时,线程B持有锁M并尝试获取锁L,那么这两个线程将永远地等待下去。这种就是最简单的死锁形式(或者叫做“抱死”)。

如图:死锁的原因就是A、B线程试图以不同的顺序来获取相同的锁。所以,所有的线程以固定的顺序来获取锁,那么在程序中就可以尽量避免出现锁顺序导致的死锁问题。

死锁实例

1)实例业务场景

我以一个经典的转账案例来进行说明,我们知道转账就是将资金从一个账户转入另一个账户。在开始转账之前,首先需要获取这两个账户的对象锁,以确保通过原子方式来更新这两个账户的余额,同时又不破坏一些不变形条件,如:转账的余额不能为负数。

代码如下:

//动态的锁的顺序死锁
public class DynamicOrderDeadlock {public static void transferMoney(Account fromAccount,Account toAccount,int amount,int from_index,int to_index) throws Exception {System.out.println("账户 "+  from_index+"~和账户~"+to_index+" ~请求锁");synchronized (fromAccount) {System.out.println(" 账户 >>>"+from_index+" <<<获得锁");synchronized (toAccount) {System.out.println("            账户     "+from_index+" & "+to_index+"都获得锁");if (fromAccount.compareTo(amount) < 0) {throw new Exception();}else {fromAccount.debit(amount);toAccount.credit(amount);}}}} static class Account {private int balance = 100000;//这里假设每个人账户里面初始化的钱private final int accNo;private static final AtomicInteger sequence = new AtomicInteger();public Account() {accNo = sequence.incrementAndGet();}void debit(int m) throws InterruptedException {Thread.sleep(5);//模拟操作时间balance = balance + m;}void credit(int m) throws InterruptedException {Thread.sleep(5);//模拟操作时间balance = balance - m;} int getBalance() {return balance;}int getAccNo() {return accNo;}public int compareTo(int money) {if (balance > money) {return 1;}else if (balance < money) {return -1;}else {return 0;}}}
}
public class DemonstrateDeadLock {private static final int NUM_THREADS = 5;private static final int NUM_ACCOUNTS = 5;private static final int NUM_ITERATIONS = 100000;public static void main(String[] args) {final Random rnd = new Random();final Account[] accounts = new Account[NUM_ACCOUNTS];for(int i = 0;i < accounts.length;i++) {accounts[i] = new Account();}class TransferThread extends Thread{@Overridepublic void run() {for(int i = 0;i < NUM_ITERATIONS;i++) { int fromAcct = rnd.nextInt(NUM_ACCOUNTS);int toAcct =rnd.nextInt(NUM_ACCOUNTS);int amount = rnd.nextInt(100);try {DynamicOrderDeadlock.transferMoney(accounts[fromAcct],accounts[toAcct], amount,fromAcct,toAcct);//InduceLockOrder.transferMoney(accounts[fromAcct],accounts[toAcct], amount);//InduceLockOrder2.transferMoney(accounts[fromAcct],accounts[toAcct], amount);}catch (Exception e) {System.out.println("发生异常-------"+e);}}}}for(int i = 0;i < NUM_THREADS;i++) {new TransferThread().start();}}}

打印结果如下:
注意:这里的结果是我把已经执行完的给删除后,只剩下导致死锁的请求.

从打印结果的图片中可以的得到结论:由于我们无法控制transferMoney中的参数的顺序,而这些参数顺序取决于外部的输入。所以两个线程同时调用transferMoney,一个线程从X向Y转账,另一个线程从Y向X转账,那么就会发生互相等待锁的情况,导致死锁。

解决问题方案:定义锁的顺序,并且整个应用中都按照这个顺序来获取锁。

方案一

使用System.identityHashCode方法,该方法返回有Object.hashCode返回的值,此时可以通过某种任意方法来决定锁的顺序。但是在极少数情况下,两个对象可能拥有相同的散列值,在这种情况下,通过给公共变量加锁来实现给锁制定顺序。所以这种方法也是用最小的代价,换来了最大的安全性。
具体代码如下

//通过锁顺序来避免死锁
public class InduceLockOrder {private static final Object tieLock = new Object();public static void transferMoney(final Account fromAcct, final Account toAcct, final int amount)throws Exception {class Helper {public void transfer() throws Exception {if (fromAcct.compareTo(amount) < 0) {throw new Exception();} else {fromAcct.debit(amount);toAcct.credit(amount);}}}int fromHash = System.identityHashCode(fromAcct);int toHash = System.identityHashCode(toAcct);if (fromHash < toHash) {synchronized (fromAcct) {synchronized (toAcct) {new Helper().transfer();}}} else if (fromHash > toHash) {synchronized (toAcct) {synchronized (fromAcct) {new Helper().transfer();}}} else {synchronized (tieLock) {synchronized (fromAcct) {synchronized (toAcct) {new Helper().transfer();}}}}}static class Account {private int balance = 100000;public Account() {}void debit(int m) throws InterruptedException {Thread.sleep(5);balance = balance + m;}void credit(int m) throws InterruptedException {Thread.sleep(5);balance = balance - m;} int getBalance() {return balance;}public int compareTo(int money) {if (balance > money) {return 1;}else if (balance < money) {return -1;}else {return 0;}}}}

方案二
在Account中包含一个唯一的,不可变的,值。比如说账号等。通过对这个值对对象进行排序。
具体代码如下:

public class InduceLockOrder2 {public static void transferMoney(final Account fromAcct, final Account toAcct, final int amount)throws Exception {class Helper {public void transfer() throws Exception {if (fromAcct.compareTo(amount) < 0) {throw new Exception();} else {fromAcct.debit(amount);toAcct.credit(amount);}}}int fromHash = fromAcct.getAccNo();int toHash = toAcct.getAccNo();if (fromHash < toHash) {synchronized (fromAcct) {synchronized (toAcct) {new Helper().transfer();}}} else if (fromHash > toHash) {synchronized (toAcct) {synchronized (fromAcct) {new Helper().transfer();}}} }static class Account {private int balance = 100000;private final int accNo;private static final AtomicInteger sequence = new AtomicInteger();public Account() {accNo = sequence.incrementAndGet();}void debit(int m) throws InterruptedException {Thread.sleep(6);balance = balance + m;}void credit(int m) throws InterruptedException {Thread.sleep(6);balance = balance - m;} int getBalance() {return balance;}int getAccNo() {return accNo;}public int compareTo(int money) {if (balance > money) {return 1;}else if (balance < money) {return -1;}else {return 0;}}}
}

2)实例业务场景

本例来自Thinking in Java,具体的业务场景如下:

有5个哲学家去就餐,但是就只有5根筷子,这5个哲学家围坐在一起,每个哲学家的左边和右边都有一根筷子。哲学家可能在思考,也可能在就餐。哲学家要就餐的话必须获取到左边和右边两根筷子,如果这个哲学家的左边或者右边的筷子已经正在被其他哲学家使用,则要就餐的哲学家必须等待其他哲学家就餐完毕释放筷子!

具体见下图:图中圆圈表示哲学家,线条表示筷子!

具体代码如下:在本例中筷子属于竞争资源,加锁的方法肯定在筷子对应的类中,定义chopstick类,表示筷子。

package thread.test.deadLock;/*** 筷子类*/
public class Chopstick {//筷子的使用状态,true-使用,false-未使用private Boolean taken = false;/*** 使用筷子的方法** @throws InterruptedException*/public synchronized void take() throws InterruptedException {//如果筷子的状态是使用,则等待while (taken) {wait();}//将筷子的状态改成使用taken = true;}/*** 放下筷子方法*/public synchronized void drop() {//将筷子的状态改成未使用taken = false;//通知其他哲学家可以使用这根筷子notifyAll();}
}

在本例中哲学家属于任务,哲学家类实现Runnable接口使用筷子即竞争资源,在run方法中哲学家先思考片刻,然后先拿右边的筷子,后拿左边的筷子,如果都拿到了那么哲学家开始吃饭,然后先释放右边的筷子再释放左边的筷子!如果不能拿到左右两根筷子,那么哲学家持有一根筷子,等待另一根筷子被其他哲学家释放!代码如下:

package thread.test.deadLock;import java.util.concurrent.TimeUnit;/*** 哲学家类*/
public class Philosopher implements Runnable {//哲学家左边的筷子private Chopstick left;//哲学家右边的筷子private Chopstick right;//哲学家编号private final int id;//哲学家思考private final int ponderFactor;public Philosopher(int id, int ponderFactor, Chopstick left, Chopstick right) {this.id = id;this.ponderFactor = ponderFactor;this.left = left;this.right = right;}/*** 哲学家思考,思考时间ponderFactor** @throws InterruptedException*/private void thinking() throws InterruptedException {//如果不思考直接跳过if (ponderFactor == 0) {return;}//哲学家思考ponderFactorTimeUnit.MILLISECONDS.sleep(ponderFactor);}/*** run方法,提交的任务如果不打断的话且没有死锁的话会一直进行下去*/@Overridepublic void run() {try {while (!Thread.interrupted()) {//哲学家先思考片刻System.out.println(this + " is thinking!");thinking();//拿右边的筷子System.out.println(this + " taking right chopstick!");right.take();System.out.println(this + " taked right chopstick!");//拿左边的筷子System.out.println(this + " taking left chopstick!");left.take();System.out.println(this + " taked left chopstick!");//吃饭System.out.println(this + " is eating!");//放下筷子right.drop();left.drop();}} catch (Exception e) {e.printStackTrace();}}public String toString() {return "Philosopher" + id;}
}

显然,定义的Philosopher很容易造成死锁,如果每个哲学家都是先拿右手边的筷子或者先拿左手边的筷子,那么5个哲学家都是持有一个筷子并等待其他哲学家释放筷子!此时,每个任务就会持有其他任务等待的锁,形成一个相互等待的连续循环,从而造成死锁!

下面演示下死锁的示例代码,代码如下:

package thread.test.deadLock;import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;/*** 死锁问题测试*/
public class PhilosopherDeadLockTest {public static void main(String... args) {//创建线程池ExecutorService executorService = Executors.newCachedThreadPool();//创建5根筷子对象Chopstick[] chopsticks = new Chopstick[5];for (int i = 0; i < 5; i++) {chopsticks[i] = new Chopstick();}//提交for (int i = 0; i < 5; i++) {executorService.submit(new Philosopher(i, 5, chopsticks[i], chopsticks[(i + 1) % 5]));}executorService.shutdown();}
}

运行下,打印结果如下:

由上图可知,程序处于阻塞状态,打印了5条Philosopher* taked right chopstick,表示这5个任务都先拿到了右边的筷子,打印了5条Philosopher* taking left chopstick,大家都没有拿到左边的筷子,等处于等待的状态!

由上图可知,程序处于阻塞状态,打印了5条Philosopher* taked right chopstick,表示这5个任务都先拿到了右边的筷子,打印了5条Philosopher* taking left chopstick,大家都没有拿到左边的筷子,等处于等待的状态!

解决方案:

知道了造成死锁的原因了,那么解决死锁问题就是让代码逻辑不会同时满足上述4条即可!

在本例中,可以让前4个哲学家先获取右边的筷子然后获取左边的筷子,让最后一个哲学家先获取左边的筷子然后获取右边的筷子,这样第5个哲学家在获取左边的筷子的时候就会被阻塞,此时有一个筷子处于未被占用的状态,第1个哲学家就可以获取到,吃完后就会释放左右两根筷子,从而不会造成死锁问题!这是通过打破上述造成死锁原因第4条的方式解决死锁问题,具体代码如下:

package thread.test.deadLock;import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;/*** 解决死锁问题测试*/
public class PhilosopherFixedDeadLockTest {public static void main(String... args) {//创建线程池ExecutorService executorService = Executors.newCachedThreadPool();//创建5根筷子对象Chopstick[] chopsticks = new Chopstick[5];for (int i = 0; i < 5; i++) {chopsticks[i] = new Chopstick();}//提交/*** 解决死锁:* 前4个哲学家先拿右边筷子,后拿左边筷子* 最后一个哲学家先拿左边筷子,后拿右边筷子*/for (int i = 0; i < 5; i++) {if (i < 4) {executorService.submit(new Philosopher(i, 5, chopsticks[i], chopsticks[(i + 1) % 5]));} else {executorService.submit(new Philosopher(i, 5, chopsticks[0], chopsticks[5]));}}executorService.shutdown();}
}

总结造成死锁的原因:

造成死锁的原因有如下4条,且必须同时满足:

1)互斥条件:任务使用的资源中至少有一个是不能共享的,本例中筷子都不能共享,需要竞争,筷子的使用和释放方法都是用了synchronized关键字修饰!

2)至少有一个任务它必须持有一个资源且正在等待获取一个当前正在被别的任务持有的资源,本例中哲学家必须持有一根筷子且其他筷子都被其他哲学家持有!

3)资源不能被任务抢占,任务必须把资源释放当做普通事件,资源只能被占用的资源释放后才能被其他任务获取到!

4)必须有循环等待,这时一个任务等待其他任务释放资源,其他任务又在等待另外一个任务释放资源,且直到最后有一个任务在等待第一个任务释放资源,使得大家都被锁住!

如何防患?

1)尽量避免使用多个锁(如果有可能的话)。

2)规范的使用多个锁,并设计好锁的获取顺序。

3)随用随放。即是,手里有锁,如果还要获得别的锁,必须释放全部资源才能各取所需。

4)规范好循环等待条件。比如,使用超时循环等待,提高程序可控性。

定位死锁(解决方案)

最常见的方式是利用jstack等工具获取线程,然后定位互相之间的依赖关系,然后找到死锁。如果是比较明显的死锁,是可以直接定位到了,类似JConsole,甚至于通过图形界面进行有限的死锁检测。排查方案:java程序死锁,3种方式快速找到死锁代码 - 路人甲Java - 博客园 (cnblogs.com)

心得:

Java对死锁没有提供语言层面上的支持,只能通过程序员通过仔细的设计来避免死锁!所以在设计多线程程序时,应该考虑造成死锁的4个条件,绝对不能让上述4个条件同时满足!

浅谈JAVA中的死锁以及解决方案相关推荐

  1. java中单例的应用_浅谈Java中单例模式的几种应用

    目录 浅谈Java中单例模式的几种应用 第一种:懒汉式 第二种:饿汉式 第三种:双重检索式 第四种:注册登记式 第五种:内部类形式 浅谈Java中单例模式的几种应用 日常开发中,为了提高我们系统中对象 ...

  2. java 中的单元测试_浅谈Java 中的单元测试

    单元测试编写 Junit 单元测试框架 对于Java语言而言,其单元测试框架,有Junit和TestNG这两种, 下面是一个典型的JUnit测试类的结构 package com.example.dem ...

  3. 浅谈Java中的Set、List、Map的区别

    就学习经验,浅谈Java中的Set,List,Map的区别,对JAVA的集合的理解是想对于数组: 数组是大小固定的,并且同一个数组只能存放类型一样的数据(基本类型/引用类型),JAVA集合可以存储和操 ...

  4. java中修饰常量的事_浅谈java中的声明常量为什么要用static修饰

    今天定义一个类常量,想着也只有这个类可以用到,就没用static关键字修饰.结果sonar代码检查提示: Rename this field "PERSON_TYPE_USER" ...

  5. 浅谈JAVA中如何利用socket进行网络编程(二)

    转自:http://developer.51cto.com/art/201106/268386.htm Socket是网络上运行的两个程序间双向通讯的一端,它既可以接受请求,也可以发送请求,利用它可以 ...

  6. scale和java比较_浅谈java中BigDecimal的equals与compareTo的区别

    这两天在处理支付金额校验的时候出现了点问题,有个金额比较我用了BigDecimal的equals方法来比较两个金额是否相等,结果导致金额比较出现错误(比如3.0与3.00的比较等). [注:以下所讲都 ...

  7. file相对路径java_浅谈java 中文件的读取File、以及相对路径的问题

    一.对于java项目中文件的读取 1.使用system 或是 系统的properties对象 ①直接是使用 string relativelypath=system.getproperty(" ...

  8. java中的强制类型转换注意事项_浅谈Java中强制类型转换的问题

    为了更好的理解我们先看下面的例子: package com.yonyou.test; import java.util.ArrayList; import java.util.Iterator; im ...

  9. 浅谈Java中的栈和堆

    人们常说堆栈堆栈,堆和栈是内存中两处不一样的地方,什么样的数据存在栈,又是什么样的数据存在堆中? 这里浅谈Java中的栈和堆 首先,将结论写在前面,后面再用例子加以验证. Java的栈中存储以下类型数 ...

最新文章

  1. [转]语音识别中区分性训练(Discriminative Training)和最大似然估计(ML)的区别...
  2. 内存泄露排查之线程泄露
  3. 一种PacBio测序数据组装得到的基因组序列的纠错方法
  4. sqoop连接hive和mysql_用Sqoop进行Hive和MySQL之间的数据互导
  5. 微信小程序一些知识点
  6. URL重写后,在有页面回发时的处理
  7. Linux(CentOS6.4)Solr4.8.1中文分词配置(IK分词)
  8. vue从入门到精通之进阶篇(一)vue-router:导航守卫
  9. CSS3笔记之基础篇(一)边框
  10. 为什么用Spring来管理Hibernate?
  11. 汇编语言:编写code段中的代码,用push指令将a段中的前8个字型数据,逆序存储b段中
  12. Hadoop学习笔记(三):作业调度器
  13. pythonnumpy算术函数_python的numpy.prod函数运行实例详解
  14. 华为U5800刷机,Root权限
  15. Module ‘/src/components/HelloWorld.vue“‘ has no default export.Vetur(1192)
  16. 解决 unity vs2017编辑器 全范围脚本报错 : predefined type 'system.object' is not defined or imported
  17. 第四届“绽放杯”5G应用征集大赛圆满落幕 中国移动参与项目获奖数量四年蝉联第一
  18. Git 安装win7
  19. win8.1怎样打开计算机名,Win8怎么打开cmd命令窗口_Win8.1打开命令提示符的方法-192路由网...
  20. 豆瓣9.8,它凭「少儿不宜」吊打所有美剧!脑洞大开必看神作!【内附资源】...

热门文章

  1. 我与ChatGPT共同学习电路的日子:Spectre DC仿真的基本原理
  2. C# 关键字extern用法
  3. 云原生架构下的 API 网关实践: Kong (二)
  4. 基于springboot+vue仓库管理系统(程序+数据库)
  5. AMOLED Demura 烧录图像控制屏幕灯珠方式
  6. Install Tengine on Centos
  7. 有效 QA 过程测量的 10 个基本指标
  8. Bootstrap 4风格的select2下拉框
  9. AppCode的一些设置
  10. 移动端vw vh适配