Java并发编程 避免活跃性问题

Java并发编程 避免活跃性问题

  • 在安全性与活跃性之间通常存在着某种制衡
  • 我们使用加锁机制来确保线程安全,但过度的使用加锁,则可能导致锁顺序死锁
  • 使用线程池和信号量来限制对资源的使用,可能导致资源死锁
  • Java应用程序无法从死锁中恢复过来

10.1 死锁

经典的哲学家进餐问题,每个人都拥有其他人需要的资源,同时又等待其他人已经拥有的资源,并且每个人在获得锁需要的资源之前都不会放弃已经拥有的资源。

数据库系统在实际中考虑了检测死锁以及从死锁中恢复,当检测到一组事务发生了死锁是,会选择一个牺牲者并放弃这个事务。

Java在解决死锁问题方法没有数据库系统那么强大,当一组线程发生死锁时,这些线程永远不能再使用了。

高负载的情况下最容易发生死锁。

10.1.1 锁顺序死锁

出现死锁的原因:两个线程试图以不同的顺序来获得相同的锁,如果按照相同的顺序来请求锁,那么就不会出现循环的加锁依赖性。

/**
 * @author MXH
 *
 * 2018年9月14日 下午10:07:26
 * 
 * 
 */
package com.mxh.ch10.lock;

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

10.1.2 动态的锁顺序死锁

/**
 * @author MXH
 *
 * 2018年9月14日 下午10:12:09
 * 
 * 
 */
package com.mxh.ch10.lock;

public class AccountService {
    public void transferMoney(Account fromAccount, Account toAccount, DollarAmount amount) {
        /*
         * 虽然锁的顺序固定,但仍然可能会导致死锁
         * 
         * 比如A向B转账,而B恰巧也向A转账,就会导致不同锁顺序
         */
        synchronized (fromAccount) {
            synchronized (toAccount) {
                // 转账行为
            }
        }
    }
}

class Account {

}

class DollarAmount {

}

解决方法:
- 对参数进行比较,固定加锁顺序。可以使用System.indentityHashCode提供的哈希值来比较,但此方法在极少数情况下返回的值可能相同。也可以使用账户的ID进行比较
- 可以使用加时赛,在获得两个Account锁之前,首先获得这个计时赛锁。

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

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

示例代码:在协作对象之间发生的死锁

/**
 * @author MXH
 *
 * 2018年9月15日 下午5:25:00
 * 
 * 
 */
package com.mxh.ch10.lock.taxi;

import java.awt.Point;

public class Taxi {
    private Point location;
    private Point destiation;
    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(destiation)) {
            dispatcher.notifyAvailable(this);
        }
    }
}

/**
 * @author MXH
 *
 * 2018年9月15日 下午5:25:37
 * 
 * 
 */
package com.mxh.ch10.lock.taxi;

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

public class Dispatcher {
    private final Set<Taxi> taxis;
    private final Set<Taxi> availableTaxis;

    public Dispatcher() {
        taxis = new HashSet<>();
        availableTaxis = new HashSet<>();
    }

    public synchronized void notifyAvailable(Taxi taxi) {
        availableTaxis.add(taxi);
    }

    public synchronized Image getImage() {
        for (Taxi taxi : taxis) {
            taxi.getLocation();
        }
        return null;
    }

}

10.1.4 开放调用

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

用开放调用改写Taxi(使用同步代码块,而不是同步方法)

public synchronized void setLocation(Point location) {
    boolean reachedDestination;
    synchronized (this) {
        this.location = location;
        reachedDestination = location.equals(destiation);
    }

    if (reachedDestination) {
        dispatcher.notifyAvailable(this);
    }
}

在程序中应该尽量使用开放调用。

10.1.5 资源死锁

  • 资源池通常采用信号量来实现
  • 资源池越大,出现死锁的可能性越小
  • 在有界、资源池与相互依赖的任务不能一起使用

10.2 死锁的避免与诊断

在设计时必须考虑锁的顺序:尽量减少潜在的解锁交互数量,将获取锁时需要遵循的协议写入正式文档并始终遵循这些协议。

首先,找出在什么地方将过去这个锁(是这个集合尽量小),然后对所有这些实例进行全局分析,从而确保它们在整个程序中获取锁的顺序都保持一致。

10.2.1 支持定时的锁

有一项技术可以检测死锁和从死锁中恢复过来,即显式使用Lock中的定时tryLock功能来代替内置锁机制。显式锁可以指定一个超时限制,超时即失败,但不会有错误信息,可以重启任务或取消。

10.2.2 通过线程转储信息类分析死锁

JVM通过线程转储来帮助识别死锁的发生。在许多IDE(集成开发环境)中都可以请求线程转储。在诊断死锁时,JVM可以帮我们做许多工作,哪些锁导致了这个问题,涉及哪些线程,它们持有哪些其它的锁,以及是否间接地给其他线程带来了不利影响。

简单方法:jps命令获取进程pid,jstack根据pid得到线程转储信息。

分析程序

/**
 * @author MXH
 *
 * 2018年9月15日 下午7:16:44
 * 
 * 
 */
package com.mxh.ch10.lock.deadlock;

public class DeadLock {
    public synchronized void work() {
        while (true) {

        }
    }

    public static void main(String[] args) {
        DeadLock deadLock = new DeadLock();
        new MyThread(deadLock).start();
        new MyThread(deadLock).start();
    }
}

class MyThread extends Thread {
    private final DeadLock deadLock;

    public MyThread(DeadLock deadLock) {
        this.deadLock = deadLock;
    }

    @Override
    public void run() {
        deadLock.work();
    }
}

获取pid(DeadLock)

查看线程转储信息

10.3 其他活跃性问题

饥饿、丢失信息和活锁。

10.3.1 饥饿

当线程由于无法访问它所需要的资源而不能继续执行时,就发生了饥饿。在Java应用程序中国对线程的优先级使用不当,或者在持有锁时执行了一些无法结束的结构,也有可能导致饥饿。

当改变了线程优先级时,由于JVM与不同操作系统的线程等级映射不同,这时候程序就成了与平台相关的。当提高某个线程的优先级时,可能不会起任何作用,或者也可能使得某个线程的调度高于其他线程,从而导致饥饿。

要避免使用线程优先级,因为这会增加平台依赖性,并可能导致活跃性问题。

10.3.2 糟糕的响应性

在GUI程序中,若使用的后台线程是CPU密集型的,那么界面线程仍然可能会失去响应。这时候就应该降低后台线程的优先级,提高界面的响应性。

10.3.3 活锁

线程重复进行一个任务,但总会失败。不会阻塞但也不会成功。这种形式的活锁通常是由于过度的错误恢复代码造成的,因为它错误地将不可修复的错误作为可修复的错误。

还有一种情况,两个线程在一个地方反复相碰。这时候就要在重试机制中引入随机性,线程分别等待一段随机的时间,避免再次相碰。

小结

出现活跃性故障时,只能中止程序。最常见的的活跃性障碍是锁顺序死锁,在设计时要确保获取多个锁时采用一致的顺序,最好的解决方案是始终开发调用。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值