在安全性和活跃性之间通常存在着某种制衡。我们使用加锁机制确保线程安全,但如果过度使用加锁,则可能导致锁顺序死锁。同样,我们使用线程池和信号量来限制对资源的使用,但这些被限制的行为可能会导致资源死锁。
死锁
其中多个线程由于存在环路的锁依赖关系而永远的等待下去,这种情况是最简单的死锁形式(抱死Deadly Embrace)。
锁顺序死锁
两个线程试图以不同顺序来获得相同的锁。
//容易发生死锁
public class LeftRightDeadLock{
private final Object left = new Object();
private final Object right = new Object();
public void leftRight(){
synchronized(left){
synchronized(right){
doSomething();
}
}
}
public void rightLeft(){
synchronized(right){
synchronized(left){
doSomethingElse();
}
}
}
}
动态的锁顺序死锁
//注意容易发生死锁
public void transferMoney(Account from,Account to,DollarAmount amount) throws InsufficientFundsException{
synchronized(from){
synchronzied(to){
if(from.getBalance().compareTo(amount) < 0){
throw new InsufficientFundsException();
} else {
from.debit(amount);
to.credit(amount);
}
}
}
}
在制定锁顺序时,可以使用System.identityHashCode方法,该方法将返回由Object.hashCode返回的值。
private static final Object tieLock = new Object();
public void transferMoney(final Account from,final Account to,final DollarAmount amout) throws InsufficientFundsException{
class Helper{
public void transfer() throws InsufficientFundsException{
if(from.getBalance().compareTo(amount) < 0){
throw new InsufficientFundsException();
} else {
from.debit(amount);
to.credit(amount);
}
}
}
int fromHash = System.identityHashCode(from);
int toHash = System.indentityHashCode(to);
if(fromHash < toHash){
synchronized(from){
synchronized(to){
new Helper().transfer();
}
}
} else if(fromHash > toHash){
synchronized(to){
synchronized(fo){
new Helper().transfer();
}
}
} else {
synchronized(tieLock) {
synchronized(from){
synchronized(to){
new Helper().transfer();
}
}
}
}
}
在极少数情况下,两个对象可能拥有相同的散列值,此时必须通过某种任意方法来决定锁的顺序,而这可能又会重新引入死锁。为了避免这种情况,可以使用“加时赛(Tie Breaking)”锁。在获得两个Account锁之前,首先获得这个“加时赛”锁,从而保证每次只有一个线程以未知的顺序获得这两个锁,从而消除了死锁的可能性。
在协作对象之间发生的死锁
Taxi表示一个出租车对象,包含位置和目的地两个属性,Dispatcher代表一个出租车车队。
class Taxi{
private Point location,destination;
private final Dispatcher dispatcher;
public Taxi(Dispatcher dispatcher){
this.dispatcher = dispatcher;
}
public synchronized Point getLocation(){
return location;
}
public synchronzied void setLocation(Point location){
this.location = location;
if(location.equals(destination){
dispathcer.notifyAvailable(this);
}
}
}
public class Dispatcher{
private final Set<Taxi> taxis;
private final Set<Taxi> availableTaxis;
public Dispatcher(){
taxis = new HashSet<Taxi>();
availableTaxis = new HashSet<Taxi>();
}
public synchronized void notifyAvailable(Taxi taxi){
availableTaxis.add(taxi);
}
public synchronzied void Image getImage(){
Image image = new Image();
for(Taxi t : taxis){
image.drawMarker(t.getLocation());
}
return image;
}
}
在调用setLocation时,会先持有Taxi的锁,然后再获取Dispatcher锁。而,调用getLocation,会先获取Dispatcher锁,然后再获取Taxi的锁。
如果在持有锁时调用某个外部方法,那么将出现活跃性问题。在这个外部方法中可能会获取其他锁(着可能产生死锁),或者阻塞时间过长,导致其他线程无法及时获得当前被持有的锁。
开放调用
如果在调用某个方法时不需要持有锁,那么这种调用被称为开放调用(Open Call)
class Taxi{
private Point location,destination;
private final Dispatcher dispatcher;
public Taxi(Dispatcher dispatcher){
this.dispatcher = dispatcher;
}
public synchronized Point getLocation(){
return location;
}
public void setLocation(Point location){
boolean reachedDestination;
synchronized(this){
this.location = location;
reachedDestination = location.equals(destination);
}
if(reachedDestination){
dispathcer.notifyAvailable(this);
}
}
}
public class Dispatcher{
private final Set<Taxi> taxis;
private final Set<Taxi> availableTaxis;
public Dispatcher(){
taxis = new HashSet<Taxi>();
availableTaxis = new HashSet<Taxi>();
}
public synchronized void notifyAvailable(Taxi taxi){
availableTaxis.add(taxi);
}
public synchronzied void Image getImage(){
Set<Taxi> copy;
synchronized(this){
copy = new HashSet<>(taxis);
}
Image image = new Image();
for(Taxi t : copy){
image.drawMarker(t.getLocation());
}
return image;
}
}
资源死锁
多个线程相互持有彼此正在等待的锁而又不释放自己已持有的锁时会发生死锁,当他们在相同的资源集合上等待时,也会发生死锁。
有界线程池/资源池和相互依赖的任务不能一起使用
死锁的避免和诊断
如果一个程序每次至多只能获得一个锁,那么就不会产生锁顺序死锁.如果必须获取多个锁,那么在设计时必须考虑锁的顺序:尽量减少潜在的加锁交互数量,将获取锁时需要遵循的协议写入文档并始终遵循这些协议。
在使用细粒度锁的程序程序中,可以通过使用一种两阶段策略来检查代码中的死锁:首先,找出在什么地方将获取多个锁,然后对所有这些实例进行全局分析,从而确保他们在整个程序中获取锁的顺序都是保持一致的。尽可能的使用开放调用。
支持定时的锁
当使用内置锁时,只要没有获得锁,就会永远等待下去,而显式锁则可以指定一个超时时限,在迭代超过该时间后tryLock会返回一个失败信息。
当定时锁失败时,你能记录发生的失败和相关信息,并通过一种更平缓的方式来重新启动计算,而不是关闭整个进程。
使用线程转储信息来分析死锁
JVM使用线程转储(Thread Dump)来帮助识别死锁的发生。
线程转储包括各个运行中的线程的栈追踪信息、加锁信息等,例如每个线程持有了哪些锁,在哪些栈帧中获得这些锁,已经被阻塞的线程正在等待获取哪一个锁。
在UNIX平台下触发线程转储操作,可以通过向JVM进程发送SIGQUIT信号(kill -3).或者在UNIX平台中按下CTRL+,在Windows平台中按下CTRL+BREAK(用笔记本的同学,方法是按下“Ctrl+Fn+b”组合键;),IDE中大多都可以请求线程转储。
Java stack information for the threads listed above:
===================================================
"Thread-1":
at Thread_learning.DeadThread$2.run(DeadThread.java:35)
- waiting to lock <0x00000000d5f05cd0> (a java.lang.Object)
- locked <0x00000000d5f05ce0> (a java.lang.Object)
at java.lang.Thread.run(Unknown Source)
"Thread-0":
at Thread_learning.DeadThread$1.run(DeadThread.java:25)
- waiting to lock <0x00000000d5f05ce0> (a java.lang.Object)
- locked <0x00000000d5f05cd0> (a java.lang.Object)
at java.lang.Thread.run(Unknown Source)
Found 1 deadlock.
其他活跃性危险
饥饿
当线程由于无法访问它所需要的资源而不能继续执行时,就发生了“饥饿”。引发饥饿最常见资源就是CPU时钟周期。如果在JAVA应用程序中对线程的优先级使用不当,或者在持有锁时执行一些无法结束的结构(例如无限循环,或者无限制的等待某个资源),那么也可能导致饥饿,因为其他需要这个锁的线程将无法获得它。
程序在一些奇怪的地方调用Thread.sleep或Thread.yield,这是因为该程序试图克服优先级调整问题或响应性问题,并试图让低优先级的线程执行更多时间。
要避免使用线程优先级,因为这会增加平台依赖性,并可能导致活跃性问题。
活锁
活锁不会阻塞线程,但也不能继续执行,因为线程将不断重复执行系统的操作,而且总会失败。活锁通常发生在处理事务消息的应用程序中:如果不能成功的处理某个消息,那么消息处理机制将回滚整个事务,并将它重新放到队列的头部。处理器反复调用队列头部任务,又将反复失败。
当多个相互协作的线程都对彼此进行响应从而修改各自的状态,并使得任何一个线程都无法继续执行时,就发生了活锁。
要解决活锁问题,通常在重试机制中引入随机性。