Java的死锁及解决思路(延伸: 活锁,饥饿,无锁)

死锁

A线程持有 锁1,接下来要获取锁2;与此同时,B线程持有锁2,要获取锁1。两个线程都在等对方释放自己需要的锁,这时两方会永远等待下去,就形成了死锁。

死锁的四个必要条件:

1.互斥:资源(锁)同时只能被一个线程占用。

2.占有且等待:线程已经占用资源A,同时等待资源B时,不释放资源A。

3.不可抢占:其他线程不能强行获取当前线程占有的资源

4.循环等待:存在一个等待链,即T1等待T2占有的资源,T2等待T3占有的资源,T3等待T1占有的资源。

如果要解决死锁,则需要破坏任意一死锁的必要条件。

一.破坏占有且等待条件

 解决方法:只要限定所有资源锁同时获取,同时释放。就可以预防掉死锁。其实就是破坏掉占有且等待条件。

下面以银行转账的代码为例子

/**
* 锁分配类(单例)
*
* @author Liumz
* @since 2019-04-02 15:57:32
*/
@Component
public class Allocator {
/**
* 已被申请锁的集合
*/
private List<Object> locks = new ArrayList<>();
/**
* 申请锁
*
* @param timeOut 过期时间(秒)
* @param lockArray 要申请的锁集合
*/
public synchronized void apply(int timeOut, Object... lockArray) throws Exception {
//如果当前不满足申请条件,则等待。直到资源被释放时进行notifyAll唤醒当前线程
//while(condition){wait()} 是一个标准范式,线程如果被唤醒,执行时会再判断一次条件。
LocalDateTime dtStart = LocalDateTime.now();
while (Arrays.stream(lockArray).anyMatch(i -> this.locks.contains(i))) {
//时间间隔达到5秒还未获取到条件锁,则抛出异常
if (Duration.between(dtStart, LocalDateTime.now()).toMillis() > timeOut * 1000) {
throw new Exception("放弃任务");
}
//释放当前对象锁,并等待
try {
this.wait(1000);
} catch (InterruptedException ignore) {
}
}
//如果已被申请锁的集合中没有要申请的锁,表示申请成功,并把申请成功的锁加入集合
this.locks.addAll(Arrays.asList(lockArray));
}
/**
* 释放锁
*
* @param lockArray 要释放的锁集合
*/
public synchronized void free(Object... lockArray) {
for (Object o : lockArray) {
this.locks.remove(o);
}
//唤醒所有wait的线程,正在等待locks被移除释放的线程。尽量使用notifyAll,避免有的线程会不被唤醒,一直wait
this.notifyAll();
}
}
/**
* 银行账户类
*
* @author Liumz
* @since 2019-04-02 15:36:15
*/
@Component
@Scope("prototype")
public class BankAccount {
/**
* 余额
*/
private int balance;
/**
* 锁分配对象
*/
@Autowired
private Allocator allocator;
/**
* 转账
*
* @param target 目标账户
* @param amount 转账金额
*/
public void transfer(BankAccount target, int amount) {
//申请锁,如果申请不到会一直等待。除非超时时抛出异常
try {
this.allocator.apply(5, target, this);
} catch (Exception e) {
return;
}
try {
//同时锁定目标和当前账户,避免出现死锁情况.并且进行账户余额加减操作
synchronized (this) {
synchronized (target) {
this.balance -= amount;
target.balance += amount;
}
}
} finally {
//同时释放加的两个锁
this.allocator.free(target, this);
}
}
}

.破坏循环等待条件

解决方法:对锁进行排序,每次申请锁需要按从小到大顺序申请。这样就不存在循环等待了

/**
* 银行账户类
*
* @author Liumz
* @since 2019-04-02 15:36:15
*/
@Component
@Scope("prototype")
public class BankAccount {
/**
* 余额
*/
private int balance;
/**
* 序号id
*/
private int id; /**
* 转账
*
* @param target 目标账户
* @param amount 转账金额
*/
public void transfer(BankAccount target, int amount) {
//对账户序号排序
BankAccount firstLock = target;
BankAccount secondLock = this;
if (firstLock.id > secondLock.id) {
firstLock = this;
secondLock = target;
}
//先锁定序号小的账户,再锁定序号大的账户
synchronized (firstLock){
synchronized (secondLock){
this.balance -= amount;
target.balance += amount;
}
}
}
}

.破坏不可抢占条件

 解决方法: 使用 Lock 和UnLock,在finally里执行unlock,主动释放资源。此时别人就可以抢占了。

 

活锁:

多个线程获取不到资源,就放开已获得的资源,重试。相当于系统空转,一直在做无用功。

例如,行人走路相向而行,互相谦让,一直重复谦让的过程。

如以下一直死循环:

start:
p1 lock A
p2 lock B
p1 lock B failed
p2 lock A failed
p1 release A
p2 release B
goto start

解决方法:引入一些随机性,比如暂停随机时间重试。

饥饿:

1:优先级高的线程总是抢占到资源,而优先级低的线程可能会一直等待,从而无法获取资源无法执行;

2:一个线程一直不释放资源,别的线程也会出现饥饿的情况。

3:wait()等待情况下的线程一直都不被notify,而其他的线程总是能被唤醒

解决方法:引入公平锁

无锁:

CAS(campare and swap):内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做并返回false。CAS是原子操作,只有一条cpu指令

无锁即不对资源锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。一个修改操作在一个循环内进行,线程会不断的尝试修改共享资源,如果没有冲突(CAS判断)就修改成功并退出否则就会继续下一次循环尝试。

如jdk的基于CAS实现的原子操作类,就是对无锁的实现。 还有无锁队列,也是循环线程对变量进行CAS操作的数据结构。

CAS的缺点:

1.ABA问题:V值为A,T1,T2从内存取出V值为A.。然后T2 CAS修改变量V为B , 接着T2 又CAS修改变量V为A。这时T1 CAS 变量V时发现内存中V还是A ,CAS操作成功。

2.循环消耗大

3.只能保证一个共享变量的原子操作

上一篇:前端学习笔记系列一:6 一种新的css预编译器stylus


下一篇:CSS预编译器less简单用法