java中常见的死锁以及解决方法代码

在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中常见的死锁以及解决方法详解整合,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对我们网站的支持!

(0)

相关推荐

  • 利用Python+Java调用Shell脚本时的死锁陷阱详解

    前言 最近有一项需求,要定时判断任务执行条件是否满足并触发 Spark 任务,平时编写 Spark 任务时都是封装为一个 Jar 包,然后采用 Shell 脚本形式传入所需参数执行,考虑到本次判断条件逻辑复杂,只用 Shell 脚本完成不利于开发测试,所以调研使用了 Python 和 Java 分别调用 Spark 脚本的方法. 使用版本为 Python 3.6.4 及 JDK 8 Python 主要使用 subprocess 库.Python 的 API 变动比较频繁,在 3.5 之后新增了

  • Java 解决死锁的方法实例详解

    死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放.由于线程被无限期地阻塞,因此程序不可能正常终止. java 死锁产生的四个必要条件: 1>互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用 2>不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放. 3>请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的战友. 4>循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P

  • java避免死锁的常见方法代码解析

    死锁 索是一个非常有用的工具,运用场景非常多,因为它使用起来非常简单,而且易于理解.但同时它也会带来一些困扰,那就是可能会引起死锁,一旦产生死锁,就会造成系统功能不可用.让我们先来看一段代码,这段代码会引起死锁,使线程 thread_1 和线程 thread_2 互相等待对方释放锁. package thread; public class DeadLockDemo { private static String A = "A"; private static String B = &

  • java 多线程死锁详解及简单实例

    java 多线程死锁 相信有过多线程编程经验的朋友,都吃过死锁的苦.除非你不使用多线程,否则死锁的可能性会一直存在.为什么会出现死锁呢?我想原因主要有下面几个方面: (1)个人使用锁的经验差异     (2)模块使用锁的差异     (3)版本之间的差异     (4)分支之间的差异     (5)修改代码和重构代码带来的差异 不管什么原因,死锁的危机都是存在的.那么,通常出现的死锁都有哪些呢?我们可以一个一个看过来,     (1)忘记释放锁 void data_process() { Ent

  • Java多线程之死锁的出现和解决方法

    什么是死锁? 死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放.由于线程被无限期地阻塞,因此程序不能正常运行.形象的说就是:一个宝藏需要两把钥匙来打开,同时间正好来了两个人,他们一人一把钥匙,但是双方都再等着对方能交出钥匙来打开宝藏,谁都没释放自己的那把钥匙.就这样这俩人一直僵持下去,直到开发人员发现这个局面. 导致死锁的根源在于不适当地运用"synchronized"关键词来管理线程对特定对象的访问."synchronized"关

  • java中常见的死锁以及解决方法代码

    在java中我们常常使用加锁机制来确保线程安全,但是如果过度使用加锁,则可能导致锁顺序死锁.同样,我们使用线程池和信号量来限制对资源的使用,但是这些被限制的行为可能会导致资源死锁.java应用程序无法从死锁中恢复过来,因此设计时一定要排序那些可能导致死锁出现的条件. 1.一个最简单的死锁案例 当一个线程永远地持有一个锁,并且其他线程都尝试获得这个锁时,那么它们将永远被阻塞.在线程A持有锁L并想获得锁M的同时,线程B持有锁M并尝试获得锁L,那么这两个线程将永远地等待下去.这种就是最简答的死锁形式(

  • Java中浮点数精度问题的解决方法

    问题描述 在项目中用Java做浮点数计算时,发现对于4.015*100这样的计算,结果不是预料中的401.5,而是401.49999999999994.如此长的位数,对于显示来说很不友好. 问题原因:浮点数表示 查阅相关资料,发现原因是:计算机中的浮点数并不能完全精确表示.例如,对于一个double型的38414.4来说,计算机是这样存储它的: 转成二进制:1001011000001110.0110011001100110011001100110011001100 转成科 学计数法:1.0010

  • vue中常见的问题及解决方法总结(推荐)

    有一些问题不限于 Vue,还适应于其他类型的 SPA 项目. 1. 页面权限控制和登陆验证页面权限控制 页面权限控制是什么意思呢? 就是一个网站有不同的角色,比如管理员和普通用户,要求不同的角色能访问的页面是不一样的.如果一个页面,有角色越权访问,这时就得做出限制了. 一种方法是通过动态添加路由和菜单来做控制,不能访问的页面不添加到路由表里,这是其中一种办法.具体细节请看下一节的<动态菜单>. 另一种办法就是所有的页面都在路由表里,只是在访问的时候要判断一下角色权限.如果有权限就允许访问,没有

  • 理解Java中的内存泄露及解决方法示例

    本文详细地介绍了Java内存管理的原理,以及内存泄露产生的原因,同时提供了一些列解决Java内存泄露的方案,希望对各位Java开发者有所帮助. Java内存管理机制 在C++ 语言中,如果需要动态分配一块内存,程序员需要负责这块内存的整个生命周期.从申请分配.到使用.再到最后的释放.这样的过程非常灵活,但是却十分繁琐,程序员很容易由于疏忽而忘记释放内存,从而导致内存的泄露. Java 语言对内存管理做了自己的优化,这就是垃圾回收机制. Java 的几乎所有内存对象都是在堆内存上分配(基本数据类型

  • Laravel中常见的错误与解决方法小结

    一.报错: 「Can't swap PDO instance while within transaction」 通过查询 Laravel 源代码,可以确认异常是在 setPdo 方法中抛出的: <?php public function setPdo($pdo) { if ($this->transactions >= 1) { throw new RuntimeException(" Can't swap PDO instance while within transact

  • Java中设置JAVA_HOME无效的解决方法

    前言 如果你的电脑装有不止一个java环境,但是设置JAVA_HOME无效时,可以参考下面的这个方法,下面话不多说了,来一起看看详细的介绍吧. 背景 我的电脑是win7 64位系统,之前学习java时安装了java 7 配置了JAVA_HOME为64位的jdk 7 快速进入环境变量的方法: 点击win的start按钮 在搜索框中输入env,即可快速定位到环境变量选项 用cmd打开窗口,运行java -version ,可以知道当前运行的java版本是java 7 为了适配jetty服务器,又安装

  • Java工作中常见的并发问题处理方法总结

    问题复现 1. "设备Aの奇怪分身" 时间回到很久很久以前的一个深夜,那时我开发的多媒体广告播放控制系统刚刚投产上线,公司开出的第一家线下生鲜店里,几十个大大小小的多媒体硬件设备正常联网后,正由我一台一台的注册及接入到已经上线的多媒体广告播控系统中. 注册过程简述如下: 每一个设备注册到系统中后,相应的在数据库设备表中都会新增一条记录,来存储这个设备的各项信息. 本来一切都有条不紊的进行着,直到设备A的注册打破了这默契的宁静-- 设备A注册完成后,我突然发现,数据库设备表中,新增了两条

  • Java中常见死锁与活锁的实例详解

    本文介绍了Java中常见死锁与活锁的实例详解,分享给大家,具体如下: 顺序死锁:过度加锁,导致由于执行顺序的原因,互相持有对方正在等待的锁 资源死锁:多个线程在相同的资源上发生等待 由于调用顺序而产生的死锁 public class Test { Object leftLock = new Object(); Object rightLock = new Object(); public static void main(String[] args) { final Test test = ne

  • Java中java.lang.ClassCastException异常原因及解决方法

    通常我们在 OOP 设计中都会使用到继承. ​​但是在继承对象之间的强制转换可能会遇到​​java.lang.ClassCastException​​异常的错误. 错误的日志如下: 19:58:25.010 [http-nio-8080-exec-5] ERROR o.a.c.c.C.[.[.[.[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw

  • 详解Java泛型中类型擦除问题的解决方法

    以前就了解过Java泛型的实现是不完整的,最近在做一些代码重构的时候遇到一些Java泛型类型擦除的问题,简单的来说,Java泛型中所指定的类型在编译时会将其去除,因此List 和 List 在编译成字节码的时候实际上是一样的.因此java泛型只能做到编译期检查的功能,运行期间就不能保证类型安全.我最近遇到的一个问题如下: 假设有两个bean类 /** Test. */ @Data @NoArgsConstructor @AllArgsConstructor public static class

随机推荐