目录
思维导图
1 死锁
比如经典的哲学家进餐问题,很好的阐释了死锁。
如果每个人都拿起左边的筷子,等待右侧筷子可用,同时不放弃手中的筷子,将导致死锁产生。
当一个线程永远占有有一个锁,而其它线程尝试去占有这个锁,那么它们将永远被阻塞。
1.1 锁顺序死锁
比如下列demo-1:
private final Object leftLock = new Object();
private final Object rightLock = new Object();
private final Object tieLock = new Object();
public void leftOperation() {
synchronized (leftLock) {
synchronized (rightLock) {
doSomething();
}
}
}
public void rightOperation() {
synchronized (rightLock) {
synchronized (leftLock) {
doElse();
}
}
}
如果两个线程有如下的操作线将发生死锁:
主要是由于两个线程试图通过不同的顺序获取多个相同的锁。
如果所有的线程能够以固定的秩序获取锁,程序就不会出现锁顺序锁死了。
1.2 动态的锁顺序死锁
有时你并不能一目了然看清是否能避免死锁发生,如下demo-2:
//动态加锁顺序产生死锁
public void transferMoney(Account from, Account to, Integer amount) {
synchronized (from) {
synchronized (to) {
if (from.getBalance().compareTo(amount) < 0) {
throw new IllegalStateException();
} else {
from.debit(amount);
to.credit(amount);
}
}
}
}
如果发生以下序列,将产生死锁:
- A->B和B->A同时发生,同时持有锁,又互相等待锁。
我们应该制定锁的顺序,并应用到程序中,获得锁的顺序必须始终遵守这个固定顺序。
如下demo-3可以避免死锁:
//制定锁的顺序避免死锁
public void transferUpdate(Account from, Account to, Integer amount) {
class Helper{
public void transfer(Account from, Account to, Integer amount) {
if (from.getBalance().compareTo(amount) < 0) {
throw new IllegalStateException();
} else {
from.debit(amount);
to.credit(amount);
}
}
}
int fromHashCode = System.identityHashCode(from);
int toHashCode = System.identityHashCode(to);
if (fromHashCode < toHashCode) {
synchronized (from) {
synchronized (to) {
new Helper().transfer(from, to, amount);
}
}
} else if (toHashCode < fromHashCode) {
synchronized (to) {
synchronized (from) {
new Helper().transfer(from, to, amount);
}
}
} else {
synchronized (tieLock) {
synchronized (from) {
synchronized (to) {
new Helper().transfer(from, to, amount);
}
}
}
}
}
通过对象的固定hash来实现锁顺序策略,如果出现相同,意味着需要引入第三个锁防止陷入重新死锁情况。
由于
System.identityHashCode
的hash冲突概率很小,所以该技术以最小代价,换来了最大的安全性。
1.3 协作对象间的死锁
思考如下协作对象demo-3:
Taxi类。
/**
* 协作对象间加锁可能产生死锁
*/
public class Taxi {
private final Dispatcher dispatcher;
private Point location, destination;
public Taxi(Dispatcher dispatcher) {
this.dispatcher = dispatcher;
}
public synchronized Point getLocation() {
return location;
}
//顺序加锁
public synchronized void setLocation(Point location1) {
this.location = location1;
if (location1.equals(destination)) {
dispatcher.notifyAvaiable(this);
}
}
}
Dispatcher类
class Dispatcher {
private final Set<Taxi> taxis;
private final Set<Taxi> avaiableTaxis;
public Dispatcher(Set<Taxi> taxis, Set<Taxi> avaiableTaxis) {
this.taxis = taxis;
this.avaiableTaxis = avaiableTaxis;
}
public synchronized void notifyAvaiable(Taxi taxi) {
avaiableTaxis.add(taxi);
}
//顺序加锁
public synchronized Image getImage() {
Image image = new Image();
for (Taxi taxi : taxis) {
image.drawMarker(taxi.getLocation());
}
return image;
}
class Image{
public void drawMarker(Point location) {
}
}
}
虽然没有显示调用锁,但是Taxi.setLocation
方法和Dispatcher.getImage
由于获取锁仍然可能发生死锁情况:
1.4 开放调用
在持有锁的时候调用一个外部方法很难进行分析,因此相当危险。
当调用的方法不需要持有锁的时候,被称为开放调用。
上述的demo-3可以通过开放调用,减小死锁风险,做如下改进demo-4:
/**
* 开放调度,缩减synchronized块。
*/
public void setLocationNow(Point point) {
boolean isDestination;
synchronized (this) {
location = point;
isDestination = location.equals(destination) ? true:false;
}
if (isDestination) {
dispatcher.notifyAvaiable(this);
}
}
/**
* 开放调度,缩减synchronized块。
*/
public Image getImageNow() {
Image image = new Image();
Set<Taxi> copy;
synchronized (this) {
copy = new HashSet<>(taxis);
}
for (Taxi taxi : copy) {
image.drawMarker(taxi.getLocation());
}
return image;
}
通过减小synchronized块,减少死锁发生情况。
1.5 资源死锁
当持有和等待目标变为资源时,会发生类似的死锁。
比如当一个线程需要获取两个数据库连接时,如果多个线程按照随机顺序获取,大概率出现死锁。
另一种形式的基于资源的死锁是线程饥饿死锁。
2 避免和诊断死锁
首先应该设计时避免死锁:
如果你必须获取多个锁,那么设计锁的顺序就是你的工作:尽量减少锁的交互数量,遵守并文档化锁的顺序。
2.1 尝试定时的锁
可以使用juc中的Lock类代替内部锁机制,因为显示的锁可以让你定义超时事件,不至于一直等待。
如下demo-5演示:
public class LockTes {
public final Lock left = new ReentrantLock();
public final Lock right = new ReentrantLock();
public static void main(String[] args) {
LockTes lockTes = new LockTes();
Thread threadOne = new Thread(() -> {
lockTes.printOne();
});
Thread threadTwo = new Thread(() -> {
lockTes.printTwo();
});
threadOne.start();
threadTwo.start();
}
public void printOne() {
try {
left.tryLock(5L, TimeUnit.SECONDS);
Thread.sleep(2000);
System.out.println("One进入left内部");
if (!right.tryLock(5L, TimeUnit.SECONDS)) {
System.out.println("等待获取超时,放弃获取right");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
left.unlock();
}
}
public void printTwo() {
try {
right.tryLock(5L, TimeUnit.SECONDS);
Thread.sleep(10000);
System.out.println("Two进入right内部");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
right.unlock();
}
}
}
运行结果:
当线程threadOne试图获取right时,将会等待五秒然后失败,因为线程threadTwo获取right后休眠了10s。
2.2 通过线程转储分析死锁
JVM可以使用线程转储帮你识别死锁的发生。
我们以demo-1为例,如下测试的代码:
public static void main(String[] args) {
LeftRightDeadLock leftRightDeadLock = new LeftRightDeadLock();
new Thread(() -> {
try {
leftRightDeadLock.leftOperation();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
leftRightDeadLock.rightOperation();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
启动程序后,控制台通过jps获取进程PID:
之后通过jstack -F pid号
强制获取线程转储信息:
可以看此时数线程间发生的锁持有关系和死锁情况。
3 其它的活跃度危险
包括饥饿、丢失信号和活锁。
3.1 饥饿
当线程访问它需要的资源时却被永久拒绝,将会发生饥饿。
线程API定义了线程优先级作为调度参考。
java定义了10个线程优先级,有些OS本身优先级少于10个,将会导致多个java线程优先级会映射到OS相同的优先级。
通常不使用线程优先级,因为这会增加平台依赖性,并可能引起活跃度问题。
3.2 弱响应性
除饥饿外的另一个问题是弱响应性。
不良的锁管理也可能引起弱响应性:
- 比如:对一个大容器加锁迭代,迭代元素处理又占有大量时间,将导致其它想要访问改容器线程必须等待很长时间。
3.3 活锁
活锁是线程活跃度失败的另一种形式,尽管没有阻塞,线程仍不能继续进行,因为它不断尝试相同的操作,却总是失败。
比如消息队列的消息回退导致的毒药信息问题。
解决活锁的一种方案是对重试机制引入一些随机性。
参考文献
[1]. 《JAVA并发编程实战》.