并发编程之死锁详解

并发编程之死锁详解

前言:

作为开发人员对死锁肯定不陌生,即使在项目中没有遇到过,但是至少也听过。死锁的出现存在着偶然性,但并不意味着程序没有存在死锁的风险(如果使用并发编程)一旦项目中出现死锁是一件非常严重的事情,它直接回导致项目卡死直至崩溃重启。今天给大家重点分享是,死锁是如何产生、如何检测死锁、以及如何避免死锁,最后会通过实例避免死锁。

  • 死锁的定义
  • 死锁产生的原因
  • 检测死锁
  • 避免死锁

一、死锁定义

举个简单例子解释死锁:现在有一双筷子,只有同时拿到一双筷子的人才能吃饭,这个时候A,B两个人都在抢这双筷子,但是不巧的是A,B都只抢到了一只筷子,这个时候必须要其中一个人把筷子放下,让另一个人拿过去,才能凑成一双筷子吃饭,但是这个时候A,B都不愿意放下自己手上的筷子都在等对方放下,最终都没吃上饭饿死了,这个时候就形成了死锁。

通过上面的例子我们来分析一下死锁产生的必要条件

1,资源一定是要>1,比如这个时候A、B两个人抢一个勺子喝汤,这个时候就不存在死锁问题

2,线程数>1,当线程数<=1时其实就是单线程了,肯定同样不存在死锁的问题

二、死锁产生的原因

1、死锁产生的根本原因是获取锁的顺序不一致

同样拿上面一个例子来说,现在更改一下规则,一双筷子分为a1、a2两只,抢筷子必须要先抢到a1,才能去抢a2,同样是A,B两个人抢一双筷子,这个时候就不会产生死锁的问题,因为定义了枪锁的顺序,比如A抢到了a1,这个时候B只能等待a1被释放了才能去抢a1。

2、死锁的实例

(1)静态死锁

MyDeadLock .java

package com.concurrent.deadlock;

public class MyDeadLock {

	//lock1
	private final Object firstLock = new Object();
	//lock2
	private final Object secondLock = new Object();
	
	//先获取lock1,再获取lock2
	private void first2SecondLock() throws InterruptedException {
		synchronized (firstLock) {
			Thread.sleep(100);//保证获取锁时间充分
			System.out.println(Thread.currentThread().getName()+" get firstLock "
					+ "ready get secondLock...");
			synchronized (secondLock) {
				System.out.println(Thread.currentThread().getName()+" get secondLock end ");
			}
		}
	}
	//先获取lock2,再获取lock1
	private void second2First() throws InterruptedException {
		synchronized (secondLock) {
			Thread.sleep(100);//保证获取锁时间充分
			System.out.println(Thread.currentThread().getName()+" get secondLock "
					+ "ready get first lock...");
			synchronized (firstLock) {
				System.out.println(Thread.currentThread().getName()+" get firstLock end ");
			}
		}
	}
	
	public static void main(String[] args) {
		MyDeadLock myDeadLock = new MyDeadLock();
		Thread t1 = new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					myDeadLock.first2SecondLock();
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		});
		Thread t2 = new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					myDeadLock.second2First();
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		});
		t1.start();
		t2.start();
	}
	
}

 运行结果:

并发编程之死锁详解

 上面就是最简单的死锁,thread0获取了firstLock,thread1获取了secondLock,都在等对方释放。因为顺序是写死的,我这里暂时称作它为静态死锁,对应静态当然有动态死锁拉。

(2)动态死锁

静态死锁一般不会出现在我们的程序中,因为太简单了,谁也不会犯这种低级错误,出现比较多的可能是动态死锁,并且出现的概率不高,不易复现

动态死锁当然也是因为获取锁的顺序不一致导致,只是它给人造成的假象,让人认为是顺序的获取了锁。这里举个微信转账的例子,我们人为定义“获取锁顺序一致”(注意啊,这里是引号)就是每次转账先锁定转出帐户,再锁定转入帐户,理论上是做到了获取锁顺序一致性,但是当出现A向B转账的同时B也在向A转账(或者是环形死锁),就出现了死锁情况,虽然平时这种概率出现很小,但是也是存在风险,例如微信过年发红包这种概率不算小把,下面看下动态死锁部分的代码。

DynamicDeadLockTransfer .java

package com.concurrent.dynamicdeadlock.service;

import com.concurrent.dynamicdeadlock.UserAccount;

/**
 * @author hongtaolong
 * 动态死锁转账
 */
public class DynamicDeadLockTransfer implements ITransfer {

	@Override
	public void transfer(UserAccount from, UserAccount to, double amount) throws InterruptedException {
		synchronized (from) {//锁定转出帐户
			Thread.sleep(100);
			System.out.println(Thread.currentThread().getName()+" get lock:"+from.getName()+"...");
			synchronized (to) {//锁定转入帐户
				from.flyMoney(amount);
				to.addMoney(amount);
				System.out.println(Thread.currentThread().getName()+" get lock:"+to.getName()+" end");
				System.out.println("transfer success amount = "+amount);
			}
		}

	}

}

 测试代码:

DnynamicDeadLockTest .java

package com.concurrent.dynamicdeadlock;

import com.concurrent.dynamicdeadlock.service.DynamicDeadLockTransfer;
import com.concurrent.dynamicdeadlock.service.ITransfer;

public class DnynamicDeadLockTest {

	public static void main(String[] args) {
		UserAccount zhangsan = new UserAccount("zhangsan", 10000);
		UserAccount lisi = new UserAccount("lisi", 10000);
		ITransfer transfer = new DynamicDeadLockTransfer();
		TransferThread t1 = new TransferThread(zhangsan, lisi, 100, transfer);
		TransferThread t2 = new TransferThread(lisi, zhangsan, 150, transfer);
		t1.start();
		t2.start();
	}
	
	static class TransferThread extends Thread{
		private final UserAccount from;
		private final UserAccount to;
		private final double amount;
		private final ITransfer transfer;
		
		public TransferThread(UserAccount from,UserAccount to,double amount,ITransfer transfer) {
			this.from = from;
			this.to = to;
			this.amount = amount;
			this.transfer = transfer;
		}
		@Override
		public void run() {
			try {
				transfer.transfer(from, to, amount);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
}

运行结果:

 并发编程之死锁详解

 其实还是获取锁的顺序不一致导致的死锁

三、检测死锁

检测死锁的方法jdk给我们提供了好几种,有可视化工具jconsule、jvisualvm,大家可自行查阅如何使用,我今天简单介绍下通过命令行

cmd进入到jdk的bin目录

1、jps指令获取进程id

并发编程之死锁详解

2、jstack id指令查看指定的进程

并发编程之死锁详解 

并发编程之死锁详解 

这个截图已经描述的很清楚了Thrad1获取了xxxf60的锁正在等待xxxf10的锁,而Thread0正好相反

四、避免死锁

1、synchronized内置锁解决死锁

避免死锁归根结底就是保证获取锁顺序的一致性,静态的死锁比较容易避免,那么我们来看看上面转账导致导致的动态死锁如何处理。

思路:要保证获取锁顺序的一致性,我们可以从思考如何判断两个锁的不同,比如比较两个锁的hash,还有定义唯一锁的id甚至将锁实现Comparable都行,然后可以在代码中通过比较的不同来定义锁的顺序,比如获取锁总是先获取hashcode值小的,或者id小的都行,下面就是通过hash值来实现获取锁的顺序的一致性代码

SafeDynamicDeadLockTransfer .java

package com.concurrent.dynamicdeadlock.service;

import com.concurrent.dynamicdeadlock.UserAccount;

public class SafeDynamicDeadLockTransfer implements ITransfer{
	
	private Object lock = new Object();

	@Override
	public void transfer(UserAccount from, UserAccount to, double amount) throws InterruptedException {
		//用hashcode对比from和to,始终将hashcode值小的先获取
		//当然也可以再userAccount中定义一个唯一的id,然后通过id去比较,这样更简单
		int fromHashCode = from.hashCode();
		int toHashCode = to.hashCode();
		if (fromHashCode < toHashCode) {
			synchronized (from) {
				Thread.sleep(100);
				System.out.println(Thread.currentThread().getName()+" get lock:"+from.getName()+"...");
				synchronized (to) {
					from.flyMoney(amount);
					to.addMoney(amount);
					System.out.println(Thread.currentThread().getName()+" get lock:"+to.getName()+" end");
					System.out.println("transfer success amount = "+amount);
				}
			}
		}else if(toHashCode<fromHashCode) {
			synchronized (to) {
				Thread.sleep(100);
				System.out.println(Thread.currentThread().getName()+" get lock:"+from.getName()+"...");
				synchronized (from) {
					from.flyMoney(amount);
					to.addMoney(amount);
					System.out.println(Thread.currentThread().getName()+" get lock:"+to.getName()+" end");
					System.out.println("transfer success amount = "+amount);
				}
			}
		}else {//hash冲突,重新争取一个锁,效率非常低,但是出现的概率极低,所以这个逻辑运行的可能性非常小,但是还是要处理
			synchronized (lock) {
				synchronized (from) {
					Thread.sleep(100);
					System.out.println(Thread.currentThread().getName()+" get lock:"+from.getName()+"...");
					synchronized (to) {
						from.flyMoney(amount);
						to.addMoney(amount);
						System.out.println(Thread.currentThread().getName()+" get lock:"+to.getName()+" end");
						System.out.println("transfer success amount = "+amount);
					}
				}
			}
		}
		
	}
	
}

 测试代码中只需要改动一条代码

//ITransfer transfer = new DynamicDeadLockTransfer();
ITransfer transfer = new SafeDynamicDeadLockTransfer();

测试结果:

 并发编程之死锁详解

这就很自然的解决了上述死锁的问题,那么我们再思考下是否还有其他方式解决呢?我们思考一下显示锁

2、显示锁ReentrantLock来解决死锁

利用显示锁的tryLock来解决避免死锁,代码如下

SafeDynamicDeadLockTransferToo .java

package com.concurrent.dynamicdeadlock.service;

import java.util.Random;

import com.concurrent.dynamicdeadlock.UserAccount;

/**
 * @author hongtaolong
 * 使用显示锁来避免死锁
 */
public class SafeDynamicDeadLockTransferToo implements ITransfer {

	@Override
	public void transfer(UserAccount from, UserAccount to, double amount) throws InterruptedException {
		Random random = new Random();
		while(true) {
			if (from.getLock().tryLock()) {
				try {
					System.out.println(Thread.currentThread().getName()+" get lock:"+from.getLock()+"...");
					if(to.getLock().tryLock()) {
						try {
							from.flyMoney(amount);
							to.addMoney(amount);
							System.out.println(Thread.currentThread().getName()+" get lock:"+to.getLock()+" end");
							System.out.println("transfer success amount = "+amount);
							break;
						} finally {
							to.getLock().unlock();
						}
					}
				} finally {
					from.getLock().unlock();
				}
			}
			Thread.sleep(random.nextInt(10));//避免死锁影响效率
		}
		
	}

}

 测试代码换成这个

//ITransfer transfer = new SafeDynamicDeadLockTransfer();
ITransfer transfer = new SafeDynamicDeadLockTransferToo();

运行结果:

 并发编程之死锁详解

仔细看上面的输出结果,首先程序肯定是没问题的,但是从上面看出并不是一次就成功了,而是尝试了两三次,这是为什么呢?这就要简单的介绍下活锁,死锁是不好的,我们应该杜绝编写死锁的程序,但是活锁也应该尽量避免。

活锁:尝试拿锁的机制中,发生多个线程之间互相谦让,不断发生拿锁,释放锁的过程。

解决办法:每个线程休眠随机数,错开拿锁的时间。

上述这句代码就是解决活锁的效率问题

Thread.sleep(random.nextInt(10));//避免死锁影响效率

把这段代码注释一下,看看打印的结果:

并发编程之死锁详解

上面的打印结果可以看出,由于相互谦让,导致拿锁的次数太多了,非常影响效率

活锁也用一个例子来解释一下,同样是上面A,B抢一双筷子,A,B一人抢到一只,但是这个时候两个人都太客气了,都放下手上的筷子让对方拿,这就导致一直持续这个动作,很久才能一个人完整的拿到一双筷子。

 

好了,分享就到这里为止了,如有问题,欢迎指正!谢谢!

 

 

 

 

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/111188.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • mybatis if语句_java中的if else语句

    mybatis if语句_java中的if else语句https://www.cnblogs.com/buzheng/p/12485464.htmlMyBatis中if-elseif-else的使用有表user(id,name,state,sex,age)1、单个if-else使用。  根据状态不同进行查询  <selectid=”selectUserByState”resultType=”com.bz.model.entity.User”>SELECT…

    2022年9月26日
    0
  • CSS中的em运用详解,1em等于多少像素?

    今天要看完它:使用CSS也好久了,但一直都是在使用“px”来设置Web元素的相关属性,未敢使用“em”。主要原因是,对其并不什么了解,只知道一点概念性的东西,前段时间在项目中要求使用“em”作为单位设置元素,所以从头对“em”学习了一回。稍为有一点理解,今天特意整理了一份博文与大家一起分享,希望对童子们有些许的帮助。这篇教程将引导大家如何使用“em”来创建一个基本的弹性布局,从而学习其如何计算?又

    2022年4月4日
    72
  • c语言删除数组中的元素「建议收藏」

    c语言删除数组中的元素「建议收藏」删除一个元素,相同也可删除核心思想:1.找到元素用if语句2.删除就是用后面的代替该元素(需要删除的元素),用for语句3.遍历(就是用for循环看一遍数列)就可以找到想要删除的元素,4.注意最后要给末尾换成零,因为后面的是随机的不一定为零#include<stdio.h>intmain(){ inti,a[10]; intb,c; //输入数组值 printf(“输入数组的值”); for(i=0;i<10;i++) { scanf(“%d”

    2022年7月22日
    21
  • map集合根据value找key(一个key或多个key)

    map集合根据value找key(一个key或多个key)//根据value值获取到对应的一个key值publicstaticStringgetKey(HashMap&lt;String,String&gt;map,Stringvalue){Stringkey=null;//Map,HashMap并没有实现Iteratable接口.不能用于增强for循环.for(Str…

    2022年7月23日
    7
  • Anaconda 环境变量手动设置(详细)

    问题Win键+r打开运行对话框,输入cmd回车输入conda,显示:‘conda’不是内部或外部命令,也不是可运行的程序或批处理文件。主要原因是因为安装anaconda时,不是自动选择为添加到环境变量原因导致的。只要你知道这个怎么环境设置了,一般遇到类似的问题也就可以自己解决了。(比如python的安装)第一步打开控制面板,进入所有控制面板项,再进入系统,选择高级系统设置。第二步进入高级,点击环境变量。第三步在系统变量区域内选择Path,双击。第四步点击新建。第五步

    2022年4月4日
    4.6K
  • NPTL, NGPT

    NPTL, NGPT

    2021年8月14日
    80

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号