Java并发编程实战 避免活跃性危险总结

在安全性与活跃性之间通常存在着某种制衡 我们使用加锁机制来确保线程安全 但如果过度地使用加锁 则可能导致锁顺序死锁(Lock-Ordering Deadlock) 同样 我们使用线程池和信号量来限制对资源的使用 但这些被限制的行为可能会导致资源死锁(Resource Deadlock) Java应用程序无法从死锁中恢复过来 因此在设计时一定要排除那些可能导致死锁出现的条件

死锁
当一个线程永远地持有一个锁 并且其他线程都尝试获得这个锁时 那么它们将永远被阻塞 在线程A持有锁L并想获得锁M的同时 线程B持有锁M并尝试获得锁L 那么这两个线程将永远地等待下去 这种情况就是最简单的死锁形式(或者称为 抱死(DeadlyEmbrace)) 其中多个线程由于存在环路的锁依赖关系而永远地等待下去 (把每个线程假想为有向图中的一个节点 图中每条边表示的关系是:线程A等待线程B所占有的资源 如果在图中形成了一条环路 那么就存在一个死锁)

锁顺序死锁
如果所有线程以固定的顺序来获得锁 那么在程序中就不会出现锁顺序死锁问题

简单的锁顺序死锁(不要这么做)

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() {
    }
}

在LeftRightDeadlock中发生死锁的原因是:两个线程试图以不同的顺序来获得相同的锁 如果按照相同的顺序来请求锁 那么就不会出现循环的加锁依懒性 因此也就不会产生死锁 如果每个需要锁L和锁M的线程都以相同的顺序来获取L和M 那么就不会发生死锁了

动态的锁顺序死锁
有时候 并不能清楚地知道是否在锁顺序上有足够的控制权来避免死锁的发生

动态的锁顺序死锁(不要这么做)

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转账 那么就会发生死锁:
A:transferMoney(myAccount, yourAccount, 10);
B:transferMoney(yourAccount, myAccount, 20);
如果执行时序不当 那么A可能获得myAccount的锁并等待yourAccount的锁 然而B此时持有yourAccount的锁 并正在等待myAccount的锁

在制定锁的顺序时 可以使用System.identityHashCode方法 该方法将返回由Object.hashCode返回的值

通过锁顺序来避免死锁

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 {
    }
}

在极少数情况下 两个对象可能拥有相同的散列值 此时必须通过某种任意的方法来决定锁的顺序 而这可能又会重新引入死锁 为了避免这种情况 可以使用 加时赛(Tie-Breaking) 锁 在获得两个Account锁之前 首先获得这个 加时赛 锁 从而保证每次只有一个线程以未知的顺序获得这两个锁 从而消除了死锁发生的可能性(只要一致地使用这种机制) 如果经常会出现散列冲突的情况 那么这种技术可能会成为并发性的一个瓶颈(这类似于在整个程序中只有一个锁的情况) 但由于System.identityHashCode中出现散列冲突的频率非常低 因此这项技术以最小的代价 换来了最大的安全性
如果在Account中包含一个唯一的 不可变的 并且具备可比性的键值 例如账号 那么要制定锁的顺序就更加容易了:通过键值对对象进行排序 因而不需要使用 加时赛 锁

在典型条件下会发生死锁的循环

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();
    }
}

在协作对象之间发生的死锁
某些获取多个锁的操作并不像在LeftRightDeadlock或transferMoney中那么明显 这两个锁并不一定必须在同一个方法中被获取

在相互协作对象之间的锁顺序死锁(不要这么做)

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) {
        }
    }
}

在LeftRightDeadlock或transferMoney中 要查找死锁是比较简单的 只需要找出那些需要获取两个锁的方法 然而要在Taxi和Dispatcher中查找死锁则比较困难:如果在持有锁的情况下调用某个外部方法 那么就需要警惕死锁

如果在持有锁时调用某个外部方法 那么将出现活跃性问题 在这个外部方法中可能会获取其他锁(这可能会产生死锁) 或者阻塞时间过长 导致其他线程无法及时获得当前被持有的锁

开放调用
如果在调用某个方法时不需要持有锁 那么这种调用被称为开放调用(Open Call)

通过公开调用来避免在相互协作的对象之间产生死锁

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) {
        }
    }

}

在程序中应尽量使用开放调用 与那些在持有锁时调用外部方法的程序相比 更易于对依赖于开放调用的程序进行死锁分析

资源死锁
正如当多个线程相互持有彼此正在等待的锁而又不释放自己已持有的锁时会发生死锁 当它们在相同的资源集合上等待时 也会发生死锁

死锁的避免与诊断
如果一个程序每次至多只能获得一个锁 那么就不会产生锁顺序死锁 当然 这种情况通常并不现实 但如果能够避免这种情况 那么就能省去很多工作 如果必须获取多个锁 那么在设计时必须考虑锁的顺序:尽量减少潜在的加锁交互数量 将获取锁时需要遵循的协议写入正式文档并始终遵循这些协议

支持定时的锁
还有一项技术可以检测死锁和从死锁中恢复过来 即显式使用Lock类中的定时tryLock功能来代替内置锁机制 当使用内置锁时 只要没有获得锁 就会永远等待下去 而显式锁则可以指定一个超时时限(Timeout) 在等待超过该时间后tryLock会返回一个失败信息 如果超时时限比获取锁的时间要长很多 那么就可以在发生某个意外情况后重新获得控制权

通过线程转储信息来分析死锁
虽然防止死锁的主要责任在于你自己 但JVM仍然通过线程转储(Thread Dump)来帮助识别死锁的发生 线程转储包括各个运行中的线程的栈追踪信息 这类似于发生异常时的栈追踪信息 线程转储还包含加锁信息 例如每个线程持有了哪些锁 在哪些栈帧中获得这些锁 以及被阻塞的线程正在等待获取哪一个锁 在生成线程转储之前 JVM将在等待关系图中通过搜索循环来找出死锁 如果发现了一个死锁 则获取响应的死锁信息 例如在死锁中涉及哪些锁和线程 以及这个锁的获取操作位于程序的哪些位置

其他活跃性危险
尽管死锁是最常见的活跃性危险 但在并发程序中还存在一些其他的活跃性危险 包括:饥饿 丢失信号和活锁等

饥饿
当线程由于无法访问它所需要的资源而不能继续执行时 就发生了 饥饿 引发饥饿的最常见资源就是CPU时钟周期 如果在Java应用程序中对线程的优先级使用不当 或者在持有锁时执行一些无法结束的结构(例如无限循环 或者无限制地等待某个资源) 那么也可能导致饥饿 因为其他需要这个锁的线程将无法得到它

要避免使用线程优先级 因为这会增加平台依赖性 并可能导致活跃性问题 在大多数并发应用程序中 都可以使用默认的线程优先级

糟糕的响应性
除饥饿以外的另一个问题是糟糕的响应性 如果在GUI应用程序中使用了后台线程 那么这种问题是很常见的 如果由其他线程完成的工作都是后台任务 那么应该降低它们的优先级 从而提高前台程序的响应性
不良的锁管理也可能导致糟糕的响应性 如果某个线程长时间占有一个锁(或许正在对一个大容器进行迭代 并且对每个元素进行计算密集的处理) 而其他想要访问这个容器的线程就必须等待很长时间

活锁
活锁(Livelock)是另一种形式的活跃性问题 该问题尽管不会阻塞线程 但也不能继续执行 因为线程将不断重复执行相同的操作 而且总会失败 活锁通常发生在处理事务消息的应用程序中:如果不能成功地处理某个消息 那么消息处理机制将回滚整个事务 并将它重新放到队列的开头 如果消息处理器在处理某种特定类型的消息时存在错误并导致它失败 那么每当这个消息从队列中取出并传递到存在错误的处理器时 都会发生事务回滚 由于这条消息又被放回到队列开头 因此处理器将被反复调用 并返回相同的结果(有时候也被称为毒药消息 Poison Message) 虽然处理消息的线程并没有阻塞 但也无法继续执行下去 这种形式的活锁通常是由过度的错误恢复代码造成的 因为它错误地将不可修复的错误作为可修复的错误

小结
活跃性故障是一个非常严重的问题 因为当出现活跃性故障时 除了中止应用程序之外没有其他任何机制可以帮助从这种故障时恢复过来 最常见的活跃性故障就是锁顺序死锁 在设计时应该避免产生锁顺序死锁:确保线程在获取多个锁时采用一致的顺序 最好的解决方法是在程序中始终使用开放调用 这将大大减少需要同时持有多个锁的地方 也更容易发现这些地方

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值