java并发编程实践笔记五(避免活跃度危险)
目录
有两类线程的活跃度危险:一,锁顺序死锁:为保证安全性而不加考虑地滥用锁导致;二,资源死锁,例如我们使用线程池和信号量来约束资源的使用,在不当的使用上导致。
死锁
当线程A占有锁L时,想获得锁M,与此同时,线程B已经占有M锁,却想获得L锁,那么这两个线程将永远等待下去,这叫致命地拥抱(deadly embrace),是死锁最简单的形式。这可以用数学语言中图论的工具来表示和研究。
数据库系统的设计中包含了从死锁中恢复。事务集中的一个事务一直持有许多锁,直到这个事务提交才释放这些锁,那么两个事务同时工作非常有可能发生死锁。数据库会检测到这样的死锁场景(通过搜索等待关系构成的有向图检测),从而牺牲(使之退出)一个事务,让其他事务继续执行。对应用程序来讲它可以在其他事务完成的情况下重新执行被强退的事务,此时已经没有死锁的问题,可以顺利完成事务。
而java应用程序从死锁中恢复唯一的方式就是终止并重启发生死锁的线程集。
死锁往往发生在高负载、线程活跃度高的情况下。
锁顺序死锁
package net.jcip.examples;
/**
* LeftRightDeadlock
*
* Simple lock-ordering deadlock
*
* @author Brian Goetz and Tim Peierls
*/
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();
}
}
}
void doSomething() {
}
void doSomethingElse() {
}
}
如果两个线程同时分别调用leftRight和rightLeft,此时可能发生死锁,如下图的调用时序:
解决这种死锁问题的方式就是:如果所有线程都以通用的固定顺序持有锁,程序就不会出现死锁了。
动态的锁顺序死锁
package net.jcip.examples;
import java.util.concurrent.atomic.*;
/**
* DynamicOrderDeadlock
* <p/>
* Dynamic lock-ordering deadlock
*
* @author Brian Goetz and Tim Peierls
*/
public class DynamicOrderDeadlock {
// Warning: deadlock-prone!
public static void transferMoney(Account fromAccount,
Account toAccount,
DollarAmount amount)
throws InsufficientFundsException {
synchronized (fromAccount) {
synchronized (toAccount) {
if (fromAccount.getBalance().compareTo(amount) < 0)
throw new InsufficientFundsException();
else {
fromAccount.debit(amount);
toAccount.credit(amount);
}
}
}
}
static class DollarAmount implements Comparable<DollarAmount> {
// Needs implementation
public DollarAmount(int amount) {
}
public DollarAmount add(DollarAmount d) {
return null;
}
public DollarAmount subtract(DollarAmount d) {
return null;
}
public int compareTo(DollarAmount dollarAmount) {
return 0;
}
}
static class Account {
private DollarAmount balance;
private final int acctNo;
private static final AtomicInteger sequence = new AtomicInteger();
public Account() {
acctNo = sequence.incrementAndGet();
}
void debit(DollarAmount d) {
balance = balance.subtract(d);
}
void credit(DollarAmount d) {
balance = balance.add(d);
}
DollarAmount getBalance() {
return balance;
}
int getAcctNo() {
return acctNo;
}
}
static class InsufficientFundsException extends Exception {
}
}
上面的transferMoney中持有的锁根据传入的参数来决定,所以叫做动态锁。
如果两个线程同时调用transferMoney,一个X向Y转账,另一个Y向X转账,那么有可能会按照上面的偶发时序图发生死锁。
解决的基本思路也是按照第一种来,通过固定锁的顺序来解除死锁。固定动态锁的顺序的方式有:通过锁对象的hashcode来识别锁,或者通过锁的标识属性来识别锁。如下:
package net.jcip.examples;
/**
* InduceLockOrder
*
* Inducing a lock order to avoid deadlock
*
* @author Brian Goetz and Tim Peierls
*/
public class InduceLockOrder {
private static final Object tieLock = new Object();
public void transferMoney(final Account fromAcct,
final Account toAcct,
final DollarAmount amount)
throws InsufficientFundsException {
class Helper {
public void transfer() throws InsufficientFundsException {
if (fromAcct.getBalance().compareTo(amount) < 0)
throw new InsufficientFundsException();
else {
fromAcct.debit(amount);
toAcct.credit(amount);
}
}
}
int fromHash = System.identityHashCode(fromAcct);
int toHash = System.identityHashCode(toAcct);
if (fromHash < toHash) {
synchronized (fromAcct) {
synchronized (toAcct) {
new Helper().transfer();
}
}
} else if (fromHash > toHash) {
synchronized (toAcct) {
synchronized (fromAcct) {
new Helper().transfer();
}
}
} else {
synchronized (tieLock) {
synchronized (fromAcct) {
synchronized (toAcct) {
new Helper().transfer();
}
}
}
}
}
interface DollarAmount extends Comparable<DollarAmount> {
}
interface Account {
void debit(DollarAmount d);
void credit(DollarAmount d);
DollarAmount getBalance();
int getAcctNo();
}
class InsufficientFundsException extends Exception {
}
}
一个典型的由于动态锁造成的死锁场景:
package net.jcip.examples;
import java.util.*;
import net.jcip.examples.DynamicOrderDeadlock.Account;
import net.jcip.examples.DynamicOrderDeadlock.DollarAmount;
/**
* DemonstrateDeadlock
* <p/>
* Driver loop that induces deadlock under typical conditions
*
* @author Brian Goetz and Tim Peierls
*/
public class DemonstrateDeadlock {
private static final int NUM_THREADS = 20;
private static final int NUM_ACCOUNTS = 5;
private static final int NUM_ITERATIONS = 1000000;
public static void main(String[] args) {
final Random rnd = new Random();
final Account[] accounts = new Account[NUM_ACCOUNTS];
for (int i = 0; i < accounts.length; i++)
accounts[i] = new Account();
class TransferThread extends Thread {
public void run() {
for (int i = 0; i < NUM_ITERATIONS; i++) {
int fromAcct = rnd.nextInt(NUM_ACCOUNTS);
int toAcct = rnd.nextInt(NUM_ACCOUNTS);
DollarAmount amount = new DollarAmount(rnd.nextInt(1000));
try {
DynamicOrderDeadlock.transferMoney(accounts[fromAcct], accounts[toAcct], amount);
} catch (DynamicOrderDeadlock.InsufficientFundsException ignored) {
}
}
}
}
for (int i = 0; i < NUM_THREADS; i++)
new TransferThread().start();
}
}
协作对象间的死锁
package net.jcip.examples;
import java.util.*;
import net.jcip.annotations.*;
/**
* CooperatingDeadlock
* <p/>
* Lock-ordering deadlock between cooperating objects
*
* @author Brian Goetz and Tim Peierls
*/
public class CooperatingDeadlock {
// Warning: deadlock-prone!
class Taxi {
@GuardedBy("this") private Point location, destination;
private final Dispatcher dispatcher;
public Taxi(Dispatcher dispatcher) {
this.dispatcher = dispatcher;
}
public synchronized Point getLocation() {
return location;
}
public synchronized void setLocation(Point location) {
this.location = location;
if (location.equals(destination))
dispatcher.notifyAvailable(this);
}
public synchronized Point getDestination() {
return destination;
}
public synchronized void setDestination(Point destination) {
this.destination = destination;
}
}
class Dispatcher {
@GuardedBy("this") private final Set<Taxi> taxis;
@GuardedBy("this") 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 synchronized Image getImage() {
Image image = new Image();
for (Taxi t : taxis)
image.drawMarker(t.getLocation());
return image;
}
}
class Image {
public void drawMarker(Point p) {
}
}
}
如上的代码中,Taxi的setLocation加锁方法调用了Dispatcher的notifyAvailable加锁方法,而Dispatcher的getImage加锁方法调用了Taxi的getLocation加锁方法,若两个线程同时调用setLocation和getImage方法就有很大可能发生死锁。
解决这种死锁的方式要看下面的“开发调用”。
开放调用
调用的方法不需要持有锁时,这被称为开放调用(open call)。不锁定整个方法,而是使用更小的同步块来打散方法的原子操作,从而使得同步块只守护那些对共享状态的操作,不再嵌套持有其他锁的同步代码(同步方法或同步块),CooperatingDeadlock修改如下:
package net.jcip.examples;
import java.util.*;
import net.jcip.annotations.*;
/**
* CooperatingNoDeadlock
* <p/>
* Using open calls to avoiding deadlock between cooperating objects
*
* @author Brian Goetz and Tim Peierls
*/
class CooperatingNoDeadlock {
@ThreadSafe
class Taxi {
@GuardedBy("this") private Point location, destination;
private final Dispatcher dispatcher;
public Taxi(Dispatcher dispatcher) {
this.dispatcher = dispatcher;
}
public synchronized Point getLocation() {
return location;
}
public synchronized void setLocation(Point location) {
boolean reachedDestination;
synchronized (this) {
this.location = location;
reachedDestination = location.equals(destination);
}
if (reachedDestination)
dispatcher.notifyAvailable(this);
}
public synchronized Point getDestination() {
return destination;
}
public synchronized void setDestination(Point destination) {
this.destination = destination;
}
}
@ThreadSafe
class Dispatcher {
@GuardedBy("this") private final Set<Taxi> taxis;
@GuardedBy("this") 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 Image getImage() {
Set<Taxi> copy;
synchronized (this) {
copy = new HashSet<Taxi>(taxis);
}
Image image = new Image();
for (Taxi t : copy)
image.drawMarker(t.getLocation());
return image;
}
}
class Image {
public void drawMarker(Point p) {
}
}
}
对当然同步方法的原子性的打散,可能会引发安全问题,那么我们就要按照程序内部的一些构建协议来保证安全性。
资源死锁
当线程相互等待对方已经持有的锁时,会发生死锁;当线程相互等待对方已经持有的其他资源时,也会发生死锁。这类资源通常会用资源池来管理。例如是访问数据库的连接池,线程A持有连接L1,并等待连接池返回连接L2,线程B持有连接L2,并等待连接池返回连接L1。
还有一种情况是,Executor单线程策略中的死锁,这种例子已经在前面的文章中讲过。
避免和诊断死锁
对非开放调用的程序中,查找获得多重锁的实例的方法有1.复查代码,2.通过查找程序来分析字节码或者源带码来完成查找。
使用定时的锁
使用Lock类中具有定时参数的tryLock方法,这项技术只有在同时获得两把锁的情况下才有效,如果多个所在嵌套的方法中被请求,你无法仅仅通过释放外层的锁来解决死锁。
还有一种解决方式,就是使用可轮询检查的tryLock,这个例子可以参见“高级特性”部分。
通过线程转储分析死锁
JVM的线程转储功能里转储的信息有每个线程栈追踪信息,以及随之发生的异常信息,也有锁的信息,例如:那个锁有那个线程获得,阻塞正在等待的线程的锁是哪一个等等,转储时,JVM会在等待关系的有向图中查找死锁信息。
我们把经过大量测试的2个已经发布的组件,J2ee服务器,jdbc驱动,以及一个J2ee应用组合起来,在使用的过程发生了尴尬的死锁现象,我们转储的关于死锁的部分信息如下:
很明显,一个线程持有一把MumbleDBCallableStatement锁,并等待一把MumbleDBConnection,而另一个线程持有那把MumbleDBConnection锁,并等待那把MumbleDBCallableStatement锁。
其他活跃度危险
尽管死锁是一种常见的活跃度危险,但还有其他:饥饿,丢失信号(在“高级部分”有讲),活锁。
饥饿
当线程访问它所需要的资源时却被永久地拒绝,以至于不能继续进行,这样就发生了饥饿。最常见引发饥饿的资源时CPU周期。使用线程的优先级不当可能引发饥饿,我们要尽量少用或者不用线程的优先级。
不良的锁管理,会引起程序的弱响应性。如果一个线程长时间占有一把锁,其他线程就必须等待。
活锁
一个线程尽管没被阻塞,但因为处理毒药信息(poison message)不断重试相同的错误恢复操作,且不断地失败,导致这个线程不能前行。这通常发生在消息处理的应用程序中。
多个线程间为了彼此都能继续前行而修改了自己的状态,反而使得没有一个线程能前行,这也是活锁。例如以太网中,2个基站在相同的载波上发送数据包,包会冲突,从而都发现了冲突,再稍后重发,但是他们又非常精确地在一秒后重发,于是又冲突,这样的并发冲突,以太网通过随机等待和撤回来重试能有效地避免活锁的发生。