Java并发编程实战读书笔记之死锁

Posted 郭梧悠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java并发编程实战读书笔记之死锁相关的知识,希望对你有一定的参考价值。


前言

本篇学习笔记源自于《Java并发编程实战》第10章。


提示:以下是本篇文章正文内容,下面案例可供参考

一、锁顺序死锁

看下面代码,很容易造成死锁,leftRight和rightLeft方法分别获取了left和right锁,只不过二者获取锁的顺序是相反的。那么如果一个Thread调用了leftRight,另一个Thread调用了rightLeft,并且两个Thread的操作是交错执行的,那么就容易发生时所。


class LeftRightDeadlock 
	private final Object left = new Object();
	private final Object right = new Object();
	
	public void leftRight() 
		synchronized(left) 
			synchronized(right) 
				doSomethingA();
			
		
	
	
	public void rightLeft() 
		synchronized(right) 
			synchronized(left) 
				doSomethingB();
			
		
	


如下调用就能发生死锁

ThreadA----->锁住left---->尝试锁住right--->永久等待。
ThreadB----->锁住right--->尝试锁住left--->永久等待。

根本原因在于leftRight和rightLeft方法获取锁的顺序不一样。如果每个需要left锁和right锁的线程都以相同的顺序来获取left锁和right锁则不存存在此问题。所以如果所有线程以固定的顺序来获取锁,那么在程序中就不会存在因为顺序造成的死锁问题。

所以我们在这里将rightLeft改成跟leftRight一样的顺序即可:

public void rightLeft() 
		synchronized(left) 
			synchronized(right) 
				doSomethingB();
			
		
	

二、动态的锁顺序死锁。

leftRight和rightLeft方法是两个固定写死的方法,所以修改他们很容易,只需要将顺序改变成一致即可。但是,如果是下面的代码呢?是否会发生死锁呢?

	/**
	 * 转账方法
	 * @param fromAccount 谁转账
	 * @param toAccount  转账给谁
	 * @param amount 转账金额
	 */
	public void transferMoneyMoney(Account fromAccount,Account toAccount,DollarAmount amount) 
		synchronized(fromAccount) 
			synchronized(toAccount) 
				//执行转账逻辑
			
		
		
	

transferMoneyMoney方法的作用就是用来转账,参数定了转账账户、被转账账户和转账金额。乍一看没问题,但是仍然会造成死锁,死锁的原因跟leftRight和rightLeft一模一样,都是获取锁的顺序造成的:

ThreadAtransferMoneyMoney(X,Y)----->锁住X---->尝试锁住Y-->永久等待。
ThreadBtransferMoneyMoney(Y,X)------>锁住Y--->尝试锁住X--->永久等待。

前面说过只要所有线程以固定的顺序获取锁,那么就不会发生死锁,所以我们改造的方向也有了,就是要达成如下效果,也就是获取所得顺序不应该跟参数的顺序有关,而是固定顺序:

ThreadAtransferMoneyMoney(X,Y)----->锁住X---->尝试锁住Y-->转账成功。
ThreadBtransferMoneyMoney(Y,X)------>锁住X--->尝试锁住Y--->转账成功。

有了这个思路我们就可以如下改造:

	/**
	 * 转账方法
	 * @param fromAccount 谁转账
	 * @param toAccount  转账给谁
	 * @param amount 转账金额
	 */
	public void transferMoneyMoney(Account fromAccount,Account toAccount,DollarAmount amount) 
		int fromHash = System.identityHashCode(fromAccount);
		int toHash =  System.identityHashCode(toHash);
		
		if(fromHash>toHash) 
			synchronized(fromAccount) 
				synchronized(toAccount) 
					//执行转账逻辑
				
			
		else if(fromHash<toHash)//说明参数顺序发生改变
			synchronized(toAccount) 
				synchronized(fromAccount) 
					//执行转账逻辑
				
			
		
		
	

根据账号的HashCode大小来判断transferMoneyMoney(X,Y)还是transferMoneyMoney(Y,X).
我们假设xHash>yHash:

ThreadAtransferMoneyMoney(X,Y)----->锁住X---->尝试锁住Y-->转账成功。
ThreadBtransferMoneyMoney(Y,X)------>锁住X--->尝试锁住Y--->转账成功。

如果xHash<yHash,那么:

ThreadAtransferMoneyMoney(X,Y)----->锁住Y---->尝试锁住X-->转账成功。
ThreadBtransferMoneyMoney(Y,X)------>锁住Y--->尝试锁住X--->转账成功。

可以看出不管fromAccount和toAccount的两个参数怎么调换,都能保证ThreadA和ThreadB获取锁的顺序是一致的,不会造成死锁问题。

下面在处理下fromAccount.hashCode和toAccount.hashCode相等的情况,这个情况好判断,在加一把锁就可以了:

   /**
	 * 转账方法
	 * @param fromAccount 谁转账
	 * @param toAccount  转账给谁
	 * @param amount 转账金额
	 */
	private static final Object lock = new Object(); 
	public void transferMoneyMoney(Account fromAccount,Account toAccount,DollarAmount amount) 
		if(fromHash>toHash) 	
		else if(fromHash<toHash)//说明参数顺序发生改变
		else
		  synchronized(lock)
		    synchronized(fromAccount) 
				synchronized(fromAccount) 
					//执行转账逻辑
				
			
		  
	   
	

在获取两个账号的锁之前,首先需要获取lock锁,从而保证每次只有一个线程以未知的顺序获得者两个锁。

按照《Java并发编程实战》第10.4的测试程序,说上面这么代码仍然有潜在的死锁,但是个人水平有限,暂时没看出来。如果读者发现了,不妨在评论区告知。不胜感激。

以上是关于Java并发编程实战读书笔记之死锁的主要内容,如果未能解决你的问题,请参考以下文章

《java并发编程实战》读书笔记8--死锁,性能与可伸缩性,锁粒度锁分解锁分段

《Java并发编程实战》第十章 避免活跃性危急 读书笔记

《Java并发编程实战》第十章 避免活跃性危急 读书笔记

《Java并发编程实战》第八章 线程池的使用 读书笔记

java并发编程实战读书笔记之FutureTask

java并发编程实战读书笔记之FutureTask