java中常见的死锁以及解决方法代码
作者:diligence-zpf 发布时间:2023-04-07 19:47:30
在java中我们常常使用加锁机制来确保线程安全,但是如果过度使用加锁,则可能导致锁顺序死锁。同样,我们使用线程池和信号量来限制对资源的使用,但是这些被限制的行为可能会导致资源死锁。java应用程序无法从死锁中恢复过来,因此设计时一定要排序那些可能导致死锁出现的条件。
1.一个最简单的死锁案例
当一个线程永远地持有一个锁,并且其他线程都尝试获得这个锁时,那么它们将永远被阻塞。在线程A持有锁L并想获得锁M的同时,线程B持有锁M并尝试获得锁L,那么这两个线程将永远地等待下去。这种就是最简答的死锁形式(或者叫做"抱死")。
2.锁顺序死锁
如图:leftRight和rightLeft这两个方法分别获得left锁和right锁。如果一个线程调用了leftRight,而另一个线程调用了rightLeft,并且这两个线程的操作是交互执行,那么它们就会发生死锁。
死锁的原因就是两个线程试图以不同的顺序来获得相同的锁。所以,如果所有的线程以固定的顺序来获得锁,那么在程序中就不会出现锁顺序死锁的问题。
2.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{
@Override
public 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.2在协作对象之间发生的死锁
如果在持有锁时调用某外部的方法,那么将出现活跃性问题。在这个外部方法中可能会获取其他的锁(这个可能产生死锁),或阻塞时间过长,导致其他线程无法及时获得当前持有的锁。
场景如下:Taxi代表出租车对象,包含当前位置和目的地。Dispatcher代表车队。当一个线程收到GPS更新事件时掉用setLocation,那么它首先更新出租车的位置,然后判断它是否到达目的地。如果已经到达,它会通知Dispatcher:它需要一个新的目的地。因为setLocation和notifyAvailable都是同步方法,因此掉用setLocation线程首先获取taxi的锁,然后在获取Dispatcher的锁。同样,掉用getImage的线程首先获取Dispatcher的锁,再获取每一个taxi的锁,这两个线程按照不同的顺序来获取锁,因此可能导致死锁。
能造成死锁的代码如下:
//会发生死锁
public class CooperatingDeadLock {
// 坐标类
class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
}
// 出租车类
class Taxi {
private Point location, destination;
private final Dispatcher dispatcher;
public Taxi(Dispatcher dispatcher) {
this.dispatcher = dispatcher;
}
public synchronized Point getLocation() {
return location;
}
public synchronized void setLocation(Point location) {
this.location = location;
if (location.equals(destination)) {
dispatcher.notifyAvailable(this);
}
}
public synchronized Point getDestination() {
return destination;
}
public synchronized void setDestination(Point destination) {
this.destination = destination;
}
}
class Dispatcher {
private final Set<Taxi> taxis;
private final Set<Taxi> availableTaxis;
public Dispatcher() {
taxis = new HashSet<>();
availableTaxis = new HashSet<>();
}
public synchronized void notifyAvailable(Taxi taxi) {
availableTaxis.add(taxi);
}
public synchronized Image getImage() {
Image image = new Image();
for(Taxi t:taxis) {
image.drawMarker(t.getLocation());
}
return image;
}
}
class Image{
public void drawMarker(Point p) {
}
}
}
解决方案:使用开放掉用。
如果再调用某个方法时不需要持有锁,那么这种调用就被称为开放掉用。这种调用能有效的避免死锁,并且易于分析线程安全。
修改后的代码如下:
//此方案不会造成死锁
public class CooperatingNoDeadlock {
// 坐标类
class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
}
// 出租车类
class Taxi {
private Point location, destination;
private final Dispatcher dispatcher;
public Taxi(Dispatcher dispatcher) {
this.dispatcher = dispatcher;
}
public synchronized Point getLocation() {
return location;
}
public void setLocation(Point location) {
boolean reachedDestination;
synchronized (this) {
this.location = location;
reachedDestination = location.equals(destination);
}
if (reachedDestination) {
dispatcher.notifyAvailable(this);
}
}
public synchronized Point getDestination() {
return destination;
}
public synchronized void setDestination(Point destination) {
this.destination = destination;
}
}
class Dispatcher {
private final Set<Taxi> taxis;
private final Set<Taxi> availableTaxis;
public Dispatcher() {
taxis = new HashSet<>();
availableTaxis = new HashSet<>();
}
public synchronized void notifyAvailable(Taxi taxi) {
availableTaxis.add(taxi);
}
public Image getImage() {
Set<Taxi> copy;
synchronized (this) {
copy = new HashSet<>(taxis);
}
Image image = new Image();
for(Taxi t:copy) {
image.drawMarker(t.getLocation());
}
return image;
}
}
class Image{
public void drawMarker(Point p) {
}
}
}
总结:活跃性故障是一个非常严重的问题,因为当出现活跃性故障时,除了终止应用程序之外没有其他任何机制可以帮助从这种故障中恢复过来。最常见的活跃性故障就是锁顺序死锁。在设计时应该避免产生顺序死锁:确保线程在获取多个锁时采用一直的顺序。最好的解决方案是在程序中始终使用开放掉用。这将大大减小需要同时持有多个锁的地方,也更容易发现这些地方。
以上所述是小编给大家介绍的java中常见的死锁以及解决方法详解整合网站的支持!
来源:https://blog.csdn.net/qdh186/article/details/86497809


猜你喜欢
- LinearLayout<?xml version="1.0" encoding="utf-8"
- 随着Android设备增多,不少网站都开始设备Android设备,而Android主流设备类型以手机和平板为主。网站在适配时通过User A
- 本文为大家分享了java interface的两个经典用法,供大家参考,具体内容如下1.Java多态接口动态加载实例编写一个通用程序,用来计
- 动态获取对象的性能值,这个在开发过程中经常会遇到,这里我们探讨一下何如高性能的获取属性值。为了对比测试,我们定义一个类Peoplepubli
- 目录一、首先导入生成二维码和微信支付环境二、在application.yml文件配置微信所有需的基本配置1.导入2.创建MyWXPayCon
- 今天碰到一个很坑的问题,折腾了五六个小时,网上也收不到答案,国外有哥们碰到了,但是看到有解决方法的回复,废话不多说了。现象:运行maven
- 模拟ThreadLocal类实现:线程范围内的共享变量,每个线程只能访问他自己的,不能访问别的线程。package com.ljq.test
- 大家好,今天我们继续来学习Android 8.0系统的适配。之前我们已经讲到了,Android 8.0系统最主要需要进行适配的地方有两处:应
- 前段时间摸索了java调用matlab东西,不说学的有多深,也算有结果了,达到目的了。也即用java程序可以调用matlab中函数了。&nb
- 本文实例为大家分享了Unity使用摄像机实现望远镜效果的具体代码,供大家参考,具体内容如下听起来挺酷炫,其实超简单,就是控制摄像机的fiel
- 本文实例为大家分享了java实现小超市程序的具体代码,供大家参考,具体内容如下一.人物包1.顾客类package person;public
- AndroidMaifest.xml中声明权限<!-- 声明所有需要的权限(包括普通权限和危险权限) --><uses-p
- 前言${} 和 #{} 都是 MyBatis 中用来替换参数的,它们都可以将用户传递过来的参数,替换到 MyBatis 最终生成的
- 最近做了关于在Android设备上外接扫码的项目,在此记录一下关于Android USB扫码枪获取内容的问题首先我这边使用是USB HID的
- 多选择结构switch语句 在java中为多路分支选择流程专门提供了switch语句,switch语句根据一个表达式的值,选择运行多个操作中
- 本文实例讲述了C#实现的sqlserver操作类。分享给大家供大家参考,具体如下:using System;using System.Col
- spring boot配置hikari连接池属性事件起因与一个简单应用经常发生Young GC,甚至在没有请求量的情况下也经常发生GC (A
- 一、作用:随机流(RandomAccessFile)不属于IO流,支持对文件的读取和写入随机访问。二、随机访问文件原理: 首先把随机访问的文
- 二叉树是一种非常重要的数据结构。本文总结了二叉树的常见操作:二叉树的构建,查找,删除,二叉树的遍历(包括前序遍历、中序遍历、后序遍历、层次遍
- 引言:编写高效简洁的C语言代码,是许多软件工程师追求的目标。本文就工作中的一些体会和经验做相关的阐述,不对的地方请各位指教。第1招:以空间换