避免活跃性危险

Java并发编程:死锁的案例与解决方案
本文深入探讨了Java并发编程中的死锁问题,包括简单的锁顺序死锁、动态的锁顺序死锁、协作对象间的死锁以及资源死锁。文章通过实例展示了死锁的产生,并提出了避免死锁的策略,如保持锁的全局顺序、开放调用以及使用定时锁。此外,还讨论了线程饥饿和活锁这两种活跃性问题及其解决方法。

死锁

死锁出现的情况

1.简单的锁顺序死锁

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

2. 动态的锁顺序死锁

还是从A账户转钱到B账户的例子。

    public void transferMoney(Account fromAcct,
                              Account toAcct,
                              DollarAmount amount) 
                              throws InsufficientFundsException {
        synchronized (fromAcct) {
            synchronized (toAcct) {
                if (fromAcct.getBalance().compareTo(amount) < 0)
                    throw new InsufficientFundsException();
                else {
                    //减去钱
                    fromAcct.debit(amount);
                    //增加钱
                    toAcct.credit(amount);
                }
            }
        }
    }
解决方案

看似所有的代码都按照 from 到 to 的方式加锁,但问题在于from和to依赖于调用者,完全存在同时调用了transferMoney(A, B)和transferMoney(B, A)的可能性。改进的措施就是将无序变有序(不管调用者如何调用,在整个程序执行期间,内部执行的顺序都将固定)

    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 {
            //当相等时,使用加时赛(Tie-Breaking)锁,保证同时只有一个线程以未知的顺序加锁。
            synchronized (tieLock) {
                synchronized (fromAcct) {
                    synchronized (toAcct) {
                        new Helper().transfer();
                    }
                }
            }
        }
    }

提取可用来比较的 hashCode 来决定顺序,如果极端情况下出现相同,使用加时赛(Tie-Breaking)锁来保证同时只有一个线程以未知的顺序加锁。

当然,在真实和开发过程中,如果存在唯一的数据值,那么直接比较整个唯一的数据值来决定加锁的顺序即可。

3. 在协作对象之间发生的死锁

此类死锁不像之前那么明显,因为其两个锁的出现不一定在同一个方法里。(这种较难辨别)。考虑下面两个相互协作的类,在出租车调度系统中可能会用到它们。Taxi代表一个出租车对象,包含位置和目的地两个属性,Dispatcher 代表一个出租车车队。

public class CooperatingDeadlock {
    // 容易发生死锁
    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;
        }

        //会获得 Taxi 和  dispatcher 的锁
        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);
        }

        // 会获得 dispatcher  和  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) {
        }
    }
}

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

解决方案----开放调用

看上面的文字,如果我们在调用某个外部方法时,自身不持有锁,是不是就会避免这种情况呐?这种就叫开放调用!

@ThreadSafe
class Taxi {
    @GuardedBy("this") private Point location, destination;
    private final 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)
            dispatcher.notifyAvailable(this);
    }
}
@ThreadSafe
class Dispatcher {
    @GuardedBy("this") private final Set<Taxi> taxis;
    @GuardedBy("this") private final Set<Taxi> availableTaxis;
    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;
    }
}

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

4.资源死锁

  1. 资源占用的死锁
  2. 线程饥饿死锁:例子:单线程 Executor,等待的资源在工作队列中。

死锁的避免与诊断

  • 避免一个线程同时获取多个锁;
  • 确保锁的顺序全局一致,尽量减少锁的交互,使用开放调用
  • 避免一个线程在锁内占有多个资源,尽量保证每个锁只占有一个资源;
  • 使用定时锁,即 lock.tryLock(timeout),这样拿不到锁就放弃,不会发生死锁一直卡在那里;
  • 对于数据库锁,加锁和解锁必须在同一个数据库连接中,否则可能会解锁失败。

诊断

  • 通过线程转储来分析

其他的活跃性危险

饥饿

  • 线程由于无法访问它所需要的资源而不能继续执行
  • 最常见资源就是 CPU 时钟周期 。如果在 Java 应用程序中对线程的优先级使用不当,或者在持有锁时执行一些无法结束的结构(例如无限循环,或者无限制地等待某个资源),那么也可能导致饥饿,因为其他需要这个锁的线程将无法得到它。
    尽量不要改变线程的优先级 。只要改变了线程的优先级,程序的行为就将与平台相关,并且会导致发生饥饿问题的风险。你经常能发现某个程序会在一些奇怪的地方调用 Thread.sleep 或 Thread.yield,这是因为该程序试图克服优先级调整问题或响应性问题,试图让低优先级的线程执行更多的时间。

活锁(处理逻辑上的一种“死锁”)

  • 当多个相互协作的线程都对彼此进行响应从而修改各自的状态,并使得任何一个线程都无法继续执行时,就发生了活锁。这就像两个过于礼貌的人在半路上面对面地相遇:他们彼此都让出对方的路,然而又在另一条路上相遇了,因此他们就这样反复地避让下去。
  • 解决方法: 在重试机制中引入随机性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值