首先说明一下,这篇博客需要:
1.需要有一定锁的基础知识,并且了解简单的锁机制,和死锁的产生原因。
2.需要一定的并发知识,及多线程知识。
3.以Java为载体
好吧,我们废话不多说,这就进入我们今天的主题。
熟悉并发编程的朋友应该对"死锁"(DeadLock)这个概念不会陌生,因为在我们的程序设计多个线程,多个锁,多个“竞态”资源的时候,如果处理不得当,就非常容易出现死锁。
熟悉JVM的朋友都知道,jdk1.2以前(绿色计划)我们的JVM采用的是用户态的线程,也就是我们Java的线程不需要操作系统的内核态线程的帮助,操作系统不会感知到用户态线程的存在。
但是在jdk1.2以后,JVM便不再采用用户态线程了,转而采用内核态线程(Kernel Thread),每一个处理器(CPU)对应多个内核态线程,每一个内核态线程对应多个轻量级进程,而我们Java中的线程就是映射到轻量级进程上的,所以,实际上就是将线程的创建,阻塞,调度等操作全部交由操作系统来完成。
不是说锁呢嘛?扯社么Java线程实现?
我们通过Java的线程实现可以发现其实底层是交给操作系统实现的,所以操作系统对死锁的处理与解决方式也就是Java对死锁的处理方式。
首先要声明一点。死锁问题是可以在操作系统层面被消灭的,比如,“银行家算法”,但是成本实在是太高了,所以我们目前的操作系统选择“鸵鸟策略”,遇到死锁不管,等着重启。
一旦死锁发生则采取专门的措施,解除死锁并以最小的代价恢复操作系统运行。死锁的解除(关键是代价最小):
1)重新启动2)撤消进程
3)剥夺资源
4)进程回退
所以我么得出结论,从实际触发,到目前为指,死锁问题是不可避免的。
银行账户类Account:
/**
* 账户类
*/
public class Account {
//账户Id
private Long id;
//账户名称
private String name;
//账户余额
private long money;
//可重入锁
private Lock lock;
public Account() {
}
public Account(Long id, String name, long money) {
this.id = id;
this.name = name;
this.money = money;
this.lock = new ReentrantLock();
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public long getMoney() {
return money;
}
public void setMoney(long money) {
this.money = money;
}
public Lock getLock() {
return lock;
}
}
转账接口类transferAccount:
/**
* 定义转账接口供银行使用
*/
public interface transferAccount {
void Dotransfer(Account from,Account to,int moeny);
}
public class BackOfDp extends Thread{
private Account zs = new Account(123456L,"zs",2000L);
private Account ls = new Account(654321L,"ls",2000L);
@Test
public void fun1() {
//创建给张三给李四转账的线程
TransferLs t1 = new TransferLs();
//创建李四给张三转账的线程
TransferZs t2 = new TransferZs();
t1.start();
t2.start();
try{
t1.join();
t2.join();
}catch (InterruptedException e){
e.printStackTrace();
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("-------------------------------------------");
System.out.println(zs.getMoney());
System.out.println(ls.getMoney());
}
/**
* 锁定逻辑是先锁支出对象,再锁存入对像
*
*/
//张三给李四转账的线程
private class TransferLs extends Thread{
@Override
public void run() {
synchronized (zs){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我拿到了zs的锁");
synchronized (ls){
System.out.println("我拿到了ls的锁");
zs.setMoney(zs.getMoney()-200);
ls.setMoney(ls.getMoney()+200);
System.out.println("从"+zs.getName()+"到"+ls.getName()+"的转账动作完成");
}
}
}
}
//李四给张三转账的线程
private class TransferZs extends Thread{
@Override
public void run() {
synchronized (ls){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我拿到了ls的锁");
synchronized (zs){
System.out.println("我拿到了zs的锁");
ls.setMoney(ls.getMoney()-200);
zs.setMoney(zs.getMoney()+200);
System.out.println("从"+ls.getName()+"到"+zs.getName()+"的转账动作完成");
}
}
}
}
}
运行结果:出现死锁。
分析原因:
从程序上看,是两个线程互相持有了对方需要获取的锁,造成方都无法获取请求的锁,造成死锁。解决思路,改变锁的获取顺序,让两个线程的获取锁的顺序一致(就是要么都一起获取zs的锁,要么都一起获取ls的锁,不能一个获取zs的锁一个获取ls的锁,这样容易导致死锁)。既然有了思路,我们就来验证一下。
只需要将ls给zs转账的线程的锁定顺序修改为与zs给ls转账的顺序一致就可以了。
//李四给张三转账的线程
private class TransferZs extends Thread{
@Override
public void run() {
synchronized (zs){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我拿到了zs的锁");
synchronized (ls){
System.out.println("我拿到了ls的锁");
zs.setMoney(zs.getMoney()-200);
ls.setMoney(ls.getMoney()+200);
System.out.println("从"+zs.getName()+"到"+ls.getName()+"的转账动作完成");
}
}
}
}
运行结果:死锁解决!
真的只要确保锁定的顺序一致就能保证不出现死锁嘛?我们还是才有上面的例子来看一看什么是动态死锁。
转账线程类Transfer_1:
/**
* 版本1的转账操作可能会产生死锁。
*/
public class Transfer_1 extends Thread implements transferAccount{
private Account from;
private Account to;
private int money;
public Transfer_1(Account from,Account to,int money){
this.from = from;
this.to = to;
this.money = money;
}
/**
* 我们计划通过加锁来实现线程安全,采用的策略是先锁定支出账户,再锁定存入账户
*
* @param from
* @param to
* @param money
*
* 运行结果显示,发生了死锁:
* 原因分析,我么虽然是根据先锁定支出账户,再锁定存入账户,但是,我们的支出,存入
* 是通过参数传递过来的,所以,虽然看起来是有先后顺序的,但是因为参数是动态的,所
* 以仍然无法保证锁定顺序,这就是动态死锁。所谓动态在我看来就是指传入参数是动态可
* 变得,顺序无法根据根据简单得参数名称判断。
*/
@Override
public void Dotransfer(Account from, Account to, int money) {
System.out.println("转账动作开始!");
//锁定支出账户
synchronized (from){
System.out.println("我拿到了from的锁:"+from.getName());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (to){
System.out.println("我拿到了to的锁:"+to.getName());
from.setMoney(from.getMoney()-money);
to.setMoney(to.getMoney()+money);
System.out.println("从"+from.getName()+"到"+to.getName()+"的转账动作完成");
}
}
}
@Override
public void run() {
Dotransfer(from,to,money);
}
}
可以看出,我么是先锁定from,再锁定to,可以说是按顺序锁定了,但是因为from和to只是参数,是可变的,所以在传入参数不同的情况下,我们的锁定顺序依然是动态可变的。这就是动态死锁的出现场景。
测试类:
public class BackOfDp extends Thread{
@Test
public void fun1() {
Account zs = new Account(123456L,"zs",2000L);
Account ls = new Account(654321L,"ls",2000L);
//创建给张三给李四转账的线程
Transfer_1 t1 = new Transfer_1(zs,ls,200);
//创建李四给张三转账的线程
Transfer_1 t2 = new Transfer_1(ls,zs,300);
t1.start();
t2.start();
try{
t1.join();
t2.join();
}catch (InterruptedException e){
e.printStackTrace();
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("-------------------------------------------");
System.out.println(zs.getMoney());
System.out.println(ls.getMoney());
}
}
运行结果:产生动态死锁
转账线程类Transfer_2:
/**
* 解决动态死锁问题。
*/
public class Transfer_2 extends Thread implements transferAccount{
private Account from;
private Account to;
private int money;
public Transfer_2(Account from,Account to,int money){
this.from = from;
this.to = to;
this.money = money;
}
//这个对象是用来当作一个锁的监视器的。解决Hash碰撞问题
private Object locktie = new Object();
/**
* 解决思路:
* 我们经过上一次得失败,明白了不能依赖参数名称简单的确定锁的顺序,因为参数是
* 具有动态性的,所以,我们改变一下思路,直接根据传入对象的hashCode()大小来
* 对锁定顺序进行排序(这里要明白的是如何排序不是关键,有序才是关键)。
*
* @param from
* @param to
* @param money
*/
@Override
public void Dotransfer(Account from, Account to, int money) {
/**
* 这里需要说明一下为什么不使用HashCode()因为HashCode方法可以被重写,
* 所以,我们无法简单的使用父类或者当前类提供的简单的hashCode()方法,
* 所以,我们就使用系统提供的identityHashCode()方法,该方法保证无论
* 你是否重写了hashCode方法,都会在虚拟机层面上调用一个名为JVM_IHashCode
* 的方法来根据对象的存储地址来获取该对象的hashCode(),HashCode如果不重写
* 的话,其实也是通过这个虚拟机层面上的方法,JVM_IHashCode()方法实现的
* 这个方法是用C++实现的。
*/
int hashform = System.identityHashCode(from);
int hashto = System.identityHashCode(to);
if(hashform>hashto){
//锁定支出账户
synchronized (from){
System.out.println("我拿到了from的锁:"+from.getName());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (to ){
System.out.println("我拿到了to的锁:"+to.getName());
from.setMoney(from.getMoney()-money);
to.setMoney(to.getMoney()+money);
System.out.println("从"+from.getName()+"到"+to.getName()+"的转账动作完成");
System.out.println(from.getMoney());
System.out.println(to.getMoney());
}
}
}else if(hashform
有的朋友会有疑问,这里为什么是if{}else if()else{}一个对象的hashCode()不是不相等吗?我在这里要说的是,hash算法,本来就是一个肯定会出现hash碰撞的算法,在我们Java中hash碰撞的概率是千万分之一左右,但是依然会出现。
hashCode()和identityHashCode()
- hashCode()方法可以被重写,所以我们在某一个有继承关系的类中是无法确认是否使用的是Object.hashCode()方法,底册通过JVM_IHashCode()方法,使用C++的函数依据该对象的存储地址获取其Hash码。
- identityHashCode()这个方法不管你又没有继承关系,有没有重写hashCode()方法,都是底层通过调用JVM_IHashCode()方法来根据对象的存储地址来获取对象的Hash码。
- 需要说明的是,不同对象的Hash码是可能相同的,大概是千万分之一左右,但是依然存在,在使用hash码时要特别注意。
测试类与前面一样,只是把Transfer_1换成Transfer_2就可以了
测试结果:动态死锁消除
转账线程类Transfer_3:
/**
* 虽然上面解决了,但是我们感觉不够优雅,是否有优雅一点的方式呢?答案是有的
* 我们下面就来看看使用第二种锁机制来实现的并发安全
*
*/
public class Transfer_3 extends Thread implements transferAccount{
private Account from;
private Account to;
private int money;
public Transfer_3(Account from,Account to,int money){
this.from = from;
this.to = to;
this.money = money;
}
/**
* 我们使用了两把API层面的可重入锁,并且都是用的是其可中断的锁等待机制,也就是
* 说当一个线程持有了一把锁后长时间无法获取另一把锁,就会自动释放其持有的锁,避免
* 造成死锁。但是很有可能造成活锁问题。
*
* @param from
* @param to
* @param moeny
*/
@Override
public void Dotransfer(Account from, Account to, int moeny) {
System.out.println("转账动作开始!");
while (true){
if(from.getLock().tryLock()){
try {
System.out.println("我拿到了From的锁:"+from.getName());
if(to.getLock().tryLock()){
try {
System.out.println("我拿到了To的锁:"+to.getName());
from.setMoney(from.getMoney()-moeny);
to.setMoney(to.getMoney()+moeny);
System.out.println("转账成功!流程结束!");
break;
}finally {
to.getLock().unlock();
}
}
}finally {
from.getLock().unlock();
}
}
}
}
@Override
public void run() {
Dotransfer(from,to,money);
}
}
上面的代码看起来很优雅,并且使用trylock确实可以有效的解决死锁问题,让我们来测试测试:
测试类只需要将Transfer_2换成Transfer_3即可。
活锁的解决:
public class Transfer_3 extends Thread implements transferAccount{
private Account from;
private Account to;
private int money;
public Transfer_3(Account from,Account to,int money){
this.from = from;
this.to = to;
this.money = money;
}
/**
* 我们使用了两把API层面的可重入锁,并且都是用的是其可中断的锁等待机制,也就是
* 说当一个线程持有了一把锁后长时间无法获取另一把锁,就会自动释放其持有的锁,避免
* 造成死锁。但是很有可能造成活锁问题。
*
* @param from
* @param to
* @param moeny
*/
@Override
public void Dotransfer(Account from, Account to, int moeny) {
System.out.println("转账动作开始!");
while (true){
if(from.getLock().tryLock()){
try {
System.out.println("我拿到了From的锁:"+from.getName());
if(to.getLock().tryLock()){
try {
System.out.println("我拿到了To的锁:"+to.getName());
from.setMoney(from.getMoney()-moeny);
to.setMoney(to.getMoney()+moeny);
System.out.println("转账成功!流程结束!");
break;
}finally {
to.getLock().unlock();
}
}
}finally {
from.getLock().unlock();
}
}
//我们在这里让锁休眠一个随机的时间,目的是错峰(不让它们同时释放,同时获取)。
try {
Thread.sleep(new Random().nextInt(10));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public void run() {
Dotransfer(from,to,money);
}
}
这样其实活锁问题就得到了缓解,但是并没有完全解决,但是对我们程序的并发性的影响已经非常低了,可以接受!