并发编程实战-避免活跃性危险


介绍了如何确保并发程序执行预期的任务,以及如何提高性能

1.死锁

多个线程由于存在环路的依赖关系而永远的等待下去,称为死锁。

1.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) {
                //doSomething
            }
        }
    }
}

上面程序发生死锁的原因是:两个线程尝试以不同的顺序获取相同的锁,相互等待而死锁。如果按照相同的顺序来请求锁,就不会出现循环的加锁依赖性,因此也不会产生死锁。

如果所有线程以固定的顺序获取锁,那么程序中就不会出现死锁的情况

1.2 动态的锁顺序死锁

    public void transferMoney(Account fromAccount,Account toAccount){ 
        synchronized (fromAccount) {
            synchronized (toAccount) {
                //do something
            }
        }
    }

上述方法,看似每个线程都是按照顺序获取fromAccount和toAccount的锁,其实锁的顺序是根据参数的传入顺序来决定的
例如相同的两个账户A,B,线程1将A作为 fromAccount,B作为toAccount,线程2将B作为formAccount,将A作为toAccount,这样两个线程就同时持有对方需要的锁而阻塞陷入死锁
要解决这个问题,必须定义锁的顺序,并在整个应用程序中都按照这个顺序来获取锁
可以使用System.identityHashCode方法指定锁的顺序。

private static final Object tieLock = new Object();
    public void transferMoney(Account fromAccount, Account toAccount) {
        class Helper {
            public void transfer() {
                //do something
            }
        }
        int fromHash = System.identityHashCode(fromAccount);
        int toHash = System.identityHashCode(toAccount);
        if (fromHash < toHash) {
            synchronized (fromAccount) {
                synchronized (toAccount) {
                    new Helper().transfer();
                }
            }
        } else if (toHash < fromHash) {
            synchronized (toAccount) {
                synchronized (fromAccount) {
                    new Helper().transfer();
                }
            }
        } else {
        	//为了避免两个hash值相同时发生死锁,定义一个tieLock锁,确保同一时刻只有一个线程以它自己的顺序执行这两个锁。
        	//为什么不直接使用这种方式?每次只有一个线程执行,性能很低,而相同hash值得情况非常少,以最小得代价换来了最大得安全性
            synchronized (tieLock) {
                synchronized (fromAccount) {
                    synchronized (toAccount) {
                        new Helper().transfer();
                    }
                }
            }
        }
    }

如果在Account中存在一个唯一的,不可变的并且具备可比性的键值(如账号),那么就不需要这么麻烦,直接锁住这个键值即可。

1.3 在协作对象之间发生死锁

两个锁并不一定必须在同一个方法中被获取。

public class Taxi {
    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);
        }
    }
    
    class Dispatcher {
        private final Set<Taxi> taxis;
        private final Set<Taxi> availableTaxis;

        public Dispatcher(Set<Taxi> taxis, Set<Taxi> availableTaxis) {
            this.taxis = taxis;
            this.availableTaxis = availableTaxis;
        }
        public synchronized void notifyAvailable(Taxi taxi) {
            availableTaxis.add(taxi);
        }
        //这个方法也会获取两个锁
        public synchronized Image getImage() {
            Image image = new Image();
            for (Taxi taxi : taxis) {
                image.drawMarket(taxi.getLocation());
            }
            return image;
        }
    }
}

上面这样的死锁排查起来比较困难:如果在存在加锁的方法中调用了外部方法,就需要警惕死锁

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

1.4 开放调用

如果在调用某个方法时不需要持有锁,那么称为开放调用。
类似于采用封闭机制来提供线程安全方法。
将上面的例子改造成开放调用:

public 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) {
            dispatcher.notifyAvailable(this);
        }
    }
    @ThreadSafe
    class Dispatcher {
        private final Set<Taxi> taxis;
        private final Set<Taxi> availableTaxis;
        public Dispatcher(Set<Taxi> taxis, Set<Taxi> availableTaxis) {
            this.taxis = taxis;
            this.availableTaxis = availableTaxis;
        }
        public synchronized void notifyAvailable(Taxi taxi) {
            availableTaxis.add(taxi);
        }
        public Image getImage() {
        	//采用深复制形式,让线程不直接操作共享变量,避免线程安全的问题
            Set<Taxi> copy;
            synchronized (this) {
                copy = new HashSet<>(taxis);
            }
            Image image = new Image();
            for (Taxi taxi : copy) {
                image.drawMarket(taxi.getLocation());
            }
            return image;
        }
    }
}

收缩同步代码块的保护范围可以提高可伸缩性。

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

1.5 资源死锁

有界线程池/资源池与相互依赖的任务不能一起使用

2.死锁的避免与诊断

如果一个程序每次至多只能获得一个锁,那么就不会产生锁顺序死锁。
如果确实需要多个锁:尽量减少潜在的加锁交互数量,将获取锁时需要遵循的协议写入正式文档并始终遵循这些协议。
两阶段策略:

  • 找出在什么地方将获取多个锁,然后对所有这些示例进行全局分析,确保它们在整个程序中获取锁的顺序都保持一致
  • 尽可能地使用开放调用

2.1 支持定时地锁

显示地使用tryLock功能来代替内置锁机制。内置锁只要没有获得锁就会一直阻塞下去,而显示锁如果超时,就会返回一个失败信息。
定时锁只有在同时获取两个锁的时候才有效,如果在嵌套方法中请求多个锁,那么就算你知道发生了异常,也无法释放上层的锁。

2.2 通过线程转储信息来分析死锁

类似于发生异常时的栈追踪信息。
线程转储还包含了加锁信息,例如每个线程持有了哪些锁,在哪些栈帧中获取这些锁,以及被阻塞的线程正在等待获取哪一个锁。

3.其它活跃性危险

饥饿,丢失信号和活锁等。

3.1 饥饿

当线程由于无法访问它所需要的资源而不能继续执行时,就发生了“饥饿”。

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

3.2 糟糕的响应性

不良的锁管理可能导致糟糕的响应性。如果某个线程长时间占有一个锁,那么其它线程必须等待很长时间。

3.3 活锁

该问题尽管不会阻塞线程,但也不会继续执行,因为线程将不断执行相同的操作,而且总会失败。
通常发生在处理消息的事务上:如果不能成功的处理某个消息,那么它会回滚整个事务,并将它重新放到队列开头。循环失败
多个相互协作的线程都对彼此进行响应从而修改各自的状态,并使得任何一个线程都无法继续执行时,就发生了活锁。
解决活锁为题,需要在重试机制中引入随机性。
例如,在网络上,两台机器尝试使用相同的载波发送数据包,那么这些数据包就会发生冲突。如果两台机器都选择在1s后重发,那么又会冲突,所以它们可以分别选择一个随机数,等待一段随机时间后发送。

小结

最常见的活跃性故障是锁顺序死锁。在设计时应该避免产生锁顺序死锁:确保线程在获取多个锁时采用一致的顺序。最好的解决办法时使用开放调用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值