java并发编程实战读书笔记之死锁(代码片段)

郭梧悠 郭梧悠     2023-02-16     435

关键词:


前言

本篇学习笔记源自于《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并发编程实战读书笔记之futuretask(代码片段)

在《AndroidAsyncTask源代码浅析》一文中介绍了AndroidAsyncTask的原理,里面也初步介绍了FutureTask的使用方法。在创建Future的时候,根据其构造器来看需要传一个Callable对象,其构造器如下。publicFutureTask(Callable<V>callable)this.callable=c... 查看详情

java并发编程实战读书笔记之futuretask(代码片段)

在《AndroidAsyncTask源代码浅析》一文中介绍了AndroidAsyncTask的原理,里面也初步介绍了FutureTask的使用方法。在创建Future的时候,根据其构造器来看需要传一个Callable对象,其构造器如下。publicFutureTask(Callable<V>callable)this.callable=c... 查看详情

java并发编程实战读书笔记executorcompletionservice(代码片段)

当我们向Executor提交一组任务,并且希望任务在完成后获得结果,此时可以考虑使用ExecutorCompletionService。ExecutorCompletionService实现了CompletionService接口。ExecutorCompletionService将Executor和BlockingQueue功能融合在一起,使用它... 查看详情

java并发编程实战读书笔记executorcompletionservice(代码片段)

当我们向Executor提交一组任务,并且希望任务在完成后获得结果,此时可以考虑使用ExecutorCompletionService。ExecutorCompletionService实现了CompletionService接口。ExecutorCompletionService将Executor和BlockingQueue功能融合在一起,使用它... 查看详情

java并发编程实战之基于生产者消费者模式的日志服务读书笔记(代码片段)

日常开发中,我们会经常跟日志系统打交道,日志系统算是典型的生产者消费者模型的系统。(多个不同的)生产者线程负责生产日志消息,这些系统扮演者生产者的角色,日志系统负责将消息队列里的日... 查看详情

java并发编程实战读书笔记(代码片段)

1、无状态对象一定是线程安全的。因为无状态的对象不包含任何变量或者其他类的引用。2、在某线程修改某变量的时候,需要通过某种方式,比如加锁,来防止其他线程使用该变量。3、在java.util.concurrent包含了一些... 查看详情

java并发编程实战读书笔记(代码片段)

1、无状态对象一定是线程安全的。因为无状态的对象不包含任何变量或者其他类的引用。2、在某线程修改某变量的时候,需要通过某种方式,比如加锁,来防止其他线程使用该变量。3、在java.util.concurrent包含了一些... 查看详情

java并发编程实战读书笔记2(代码片段)

1、volatile,是一种比sychronized关键字更轻量级的同步机制,在访问volatile变量时因为不会执行加锁操作,所以不会导致线程阻塞。volatile变量也有局限性,比如它不足以确保递增操作(++)的原子性。... 查看详情

java并发编程实战读书笔记2(代码片段)

1、volatile,是一种比sychronized关键字更轻量级的同步机制,在访问volatile变量时因为不会执行加锁操作,所以不会导致线程阻塞。volatile变量也有局限性,比如它不足以确保递增操作(++)的原子性。... 查看详情

java并发编程实战读书笔记3(代码片段)

1、ConcurrentModificationException当容器在迭代过程中被其他线程修改时,就会抛出该异常。想要避免该以后该异常,就需要在迭代的过程中持有该容器的锁。但是长时间持有容器的锁会降低程序的可伸缩性,持有锁的时间... 查看详情

java并发编程实战读书笔记3(代码片段)

1、ConcurrentModificationException当容器在迭代过程中被其他线程修改时,就会抛出该异常。想要避免该以后该异常,就需要在迭代的过程中持有该容器的锁。但是长时间持有容器的锁会降低程序的可伸缩性,持有锁的时间... 查看详情

java并发编程实战读书笔记5---executor在android中的应用(代码片段)

最近业余时间在读《Java并发编程实战》这本书,现在已经读到了第二部分。该书上有这么一段话:如果你的项目中出现这样的代码newThread(newRunnable()@Overridepublicvoidrun()).start();的话,那么你可以考虑使用Executor了这句话让我想到了... 查看详情

java并发编程实战读书笔记5---executor在android中的应用(代码片段)

最近业余时间在读《Java并发编程实战》这本书,现在已经读到了第二部分。该书上有这么一段话:如果你的项目中出现这样的代码newThread(newRunnable()@Overridepublicvoidrun()).start();的话,那么你可以考虑使用Executor了这句话让我想到了... 查看详情

实战java高并发程序设计-读书笔记(代码片段)

实战Java高并发程序设计-读书笔记第一章死锁、饥饿、活锁的概念。并发级别:阻塞、饥饿、无障碍、无锁、无等待。无障碍:是一种最弱的非阻塞调度。两个线程如果是无障碍的执行,那么他们不会因为临界区的问... 查看详情

java并发编程实战之基于生产者消费者模式的日志服务读书笔记(代码片段)

日常开发中,我们会经常跟日志系统打交道,日志系统算是典型的生产者消费者模型的系统。(多个不同的)生产者线程负责生产日志消息,这些系统扮演者生产者的角色,日志系统负责将消息队列里的日... 查看详情

《java并发编程实战》读书笔记

Java并发编程实战基础知识金句基础知识金句一个对象是否需要是线程安全的,取决于它是否被多个线程访问。Java中的主要同步机制是关键字synchronized,它提供了一种独占的加锁方式,但“同步”这个术语还包括volatil... 查看详情

《java并发编程实战》读书笔记

Java并发编程实战基础知识金句基础知识金句一个对象是否需要是线程安全的,取决于它是否被多个线程访问。Java中的主要同步机制是关键字synchronized,它提供了一种独占的加锁方式,但“同步”这个术语还包括volatil... 查看详情

《java8实战》读书笔记12:函数式编程(代码片段)

...迭代和递归13.1实现和维护系统接二手项目时,修复并发导致的缺陷是很困难的。如果你喜欢无状态的行为Java8中新增的Stream提供了强大的技术支撑,让我们无需担心锁引起的各种问题,充分发掘系统的并发能力。13.... 查看详情