并发编程之死锁详解

并发编程之死锁详解

前言:

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

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

一、死锁定义

举个简单例子解释死锁:现在有一双筷子,只有同时拿到一双筷子的人才能吃饭,这个时候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)


相关推荐

  • 一次xxoo提权[通俗易懂]

    一次xxoo提权[通俗易懂]数据库root权限。然并卵。看了一下phpinfo得知是mysql5.0的然后想要通过udf之类的提权一波,结果一执行sql语句就被狗拦截了。然而数据库这条路是GG了OS名称:MicrosoftWindowsServer2008R2Enterprise修补程序:安装了1个修补程序。然后各种e…

  • oracle创建数据库详解,详解Oracle手工创建数据库-Oracle「建议收藏」

    oracle创建数据库详解,详解Oracle手工创建数据库-Oracle「建议收藏」下面就介绍一下Oracle手动创建数据库的步骤:1、确定新建数据库名称和实例名称;2、确定数据库管理员的认证方式;3、创建初始化参数文件;www.2cto.com4、创建实例;5、连接并启动实例;6、使用createdatabase语句Oracle创建数据库;7、创建附加的表空间;8、运行脚本创建数据字典视图;下面以创建一个名为“MYNEWDB”的数据库为例1、确定数据库名称与实例名称;1.DB…

  • uniqueidentifier 数据类型(转)「建议收藏」

    uniqueidentifier 数据类型(转)「建议收藏」想要产生这种唯一标识的格式的数据:6F9619FF-8B86-D011-B42D-00C04FC964FF应该怎么做呢?=====================================

  • 菜鸟的数学建模之路(一):最短路径算法「建议收藏」

    菜鸟的数学建模之路(一):最短路径算法「建议收藏」最短路径算法主要有两种,Dijkstra算法和floyd算法,当时在学习这两种算法时经常弄混了,关于这两种算法,记得当时是在交警平台设置的那一道题目上了解到的,就去查很多资料,花了不少时间才基本了解了这两种算法的基本用法,在总结的时候,我更多的是用代码的方式去做的总结,当时想的是等到要用的时候,直接改一下数据,运行代码,得到想要的最短路径就可以了。记得我们老师说过数学建模的知识没必要过于深入的去学…

  • Ural 1025-Democrary in Danger

    Ural 1025-Democrary in Danger

  • jaxen.jar下载地址

    jaxen.jar下载地址最近在写XML文件的时候需要用到这个jar包,但是在网上下载的时候贼慢,于是在csdn上找但是都要收费,最后终于找到了,现在分享给你们。不要谢,请叫我雷锋!《jaxen》jar包下载地址…

发表回复

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

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