[Java并发编程实战]协作对象之间存在的死锁问题

百川东到海,何日复西归?少壮不努力,老大徒伤悲。———《汉乐府·长歌行》
河水东流不复返就如时间的流逝不可能停留,告诫我们年少时如果不珍惜时间努力向上,到老只能白白地悔恨自己与悲伤了。

上文讲解了锁顺序死锁发生的情况,但是还存在一些死锁的情况,不是顺序锁那么简单可以被发现。因为有可能并不是在同一个方法中显示请求两个锁,而是嵌套另一个方法去获取第二个锁。

我们举个例子来说明,下面代码清单姑且当做是表示一个出租车系统吧,Taxi 表示出租车,具有位置和方向两个属性;Dispatcher表示一组出租车。

import java.awt.Point;
import java.util.HashSet;
import java.util.Set;

public class ThreadDeadLockTest{

    class Taxi{
        private Point location, destation;

        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(destation)) {
                dispatcher.notifyAvailable(this);
            }
        }
    }

    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);
        }
        //获取同步锁,同时还会获取Taxi的同步锁
        public synchronized Image getImage() {
            Image image = new Image();
            for(Taxi t : taxis) {
                image.drawer(t.getLocation());
            }
            return image;
        }
    }
}

从上面代码可以看出,setLocation 和 getImage 方法都会获取两个锁。假如一个线程调用了 setLocation 方法,那么首先会获取到一个 Taxi 的锁,然后嵌套调用了 notifyAvailable,或获取到 Dispatcher 的锁。同样,另外一个线程调用了 getImage 方法,首先他会获取 Dispatcher 的锁,然后获取 Taxi 的锁。所以两个线程会以不同的顺序占用锁,那么就会产生死锁的情况。

线程A --> Taxi的锁 --> Dispatcher 的锁
线程B --> Dispatcher的锁 --> Taxi 的锁

对于上面这种会发生死锁的情况,往往是比较难察觉的。这告诉我们,在持有锁的时候,调用外部方法可能会获取其他锁或者遭遇严重的超时阻塞,由于你不知道外部方法的情况,这通常很难进行分析。

当调用的方法不需要持有锁时,这被称为开放调用,它有着更好的行为。如何理解这句话?让我们来重构上面的代码,使其变成开放调用,从而避免死锁问题。我们需要减少 synchronize 的使用,只守护那些调用共享状态的操作。如下代码清单所示:

import java.awt.Point;
import java.util.HashSet;
import java.util.Set;

public class ThreadDeadLockTest{

    class Taxi{
        private Point location, destation;

        private final Dispatcher dispatcher;

        public Taxi(Dispatcher dispatcher) {
            this.dispatcher = dispatcher;
        }

        //获取同步锁
        public synchronized Point getLocation() {
            return location;
        }

        //缩小锁的范围,不会同时去获取两个锁
        public void setLocation(Point location) {
            boolean isReached = false;
            //缩小锁的范围
            synchronized (this) {
                this.location = location;
                isReached = location.equals(destation);
            }
            if(isReached) {
                dispatcher.notifyAvailable(this);
            }
        }
    }

    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 Image getImage() {
            Set<Taxi> copy;
            //缩小锁的范围
            synchronized(this) {
                copy = new HashSet<Taxi>(taxis);
            }
            Image image = new Image();
            for(Taxi t : copy) {
                image.drawer(t.getLocation());
            }
            return image;
        }
    }

}

getImage 和 setLocation 都缩小了锁的范围,获取第二锁之前已经释放了第一个锁,所以不会产生顺序死锁问题了。但是可能的情况是,可能会使得一个原子操作变为非原子操作。有时候这样的损失是可以接受的。比如这里 copy 的时候,可能不是瞬时最新的值。

如果一个程序一次之多获取一个锁,那么就不会产生锁顺序死锁。所以,尽量减少潜在锁之间的交互数量,遵守并文档化该锁顺序协议,缺一不可。

本文完结。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值