并发编程之死锁详解

并发编程之死锁详解

前言:

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

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

一、死锁定义

举个简单例子解释死锁:现在有一双筷子,只有同时拿到一双筷子的人才能吃饭,这个时候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一人抢到一只,但是这个时候两个人都太客气了,都放下手上的筷子让对方拿,这就导致一直持续这个动作,很久才能一个人完整的拿到一双筷子。

 

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

 

 

 

 

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

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

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)
blank

相关推荐

  • Subprocess报FileNotFoundError

    Subprocess报FileNotFoundErrorSubprocess报FileNotFoundError代码如下:运行时报错,FileNotFoundError:pipenv解决方案:因为pipenv找不到,所以需要指定全路径​whichpipenv#结果显示/root/anaconda3/bin/pipenv#因此修改代码中pipenv为全路径的,可成功运行另外,报FileNotFoundError…

  • discuz 二次开发后台调用编辑器的方法![通俗易懂]

    discuz 二次开发后台调用编辑器的方法![通俗易懂]纠结了两个晚上,一个白天,无数次的Google,无数次的baidu,依旧没搜到合理的方案,奇怪难道没人有这个需求吗??好在功夫不负有心人,终于解决了!但是有个缺憾是无法使用图片上传功能。。但是也算不错了!有谁能解决得了这个图片上传的功能,还请分享~~~下面分享一下解决方法!showtablerow(”,array(‘class=”td27″‘,’class=”td28″‘),

  • 服务器永恒之蓝病毒解决方法_永恒之蓝病毒作者

    服务器永恒之蓝病毒解决方法_永恒之蓝病毒作者一、NSA“永恒之蓝”勒索蠕虫全球爆发2017年5月12日爆发的WannaCry勒索病毒肆虐了全球网络系统,引起各国企业和机构极大恐慌。而这次受害最严重的是Windows系统,自然也被锁定为怀疑对象,有人认为正是因为该系统对于漏洞的麻木和疏漏才导致了此次勒索病毒的蔓延。作为受害者的微软却将矛头指向美国国安局(NSA)和永恒之蓝。不法分子利用…

    2022年10月16日
  • 报错415怎么解决_服务器请求415

    报错415怎么解决_服务器请求415415错误

    2022年10月31日
  • Django(7)url命名的作用「建议收藏」

    Django(7)url命名的作用「建议收藏」前言为什么我们url需要命名呢?url命名的作用是什么?我们先来看一个案例案例我们先在一个Django项目中,创建2个App,前台front和后台cms,然后在各自app下创建urls.py文件

  • javascript取整数几种方式

    javascript取整数几种方式Math.round(num)//四舍五入Math.floor(num)//小于等于num的整数Math.ceil()//大于等于num的整数parseInt(num)//小于等于num的整数,与floor的区别是parseInt参数可以是string类型,如’5abc’返回5。

发表回复

您的电子邮箱地址不会被公开。

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