4

Java并发编程-阶段性总结:活跃性问题

 3 years ago
source link: https://segmentfault.com/a/1190000040171811
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

并发编程有 3 道关卡,分别是:安全性问题、活跃性问题、性能问题,如果三关全过,也就掌握并发编程这门高阶技能。

今天,我们就来过第二关:解决活跃性问题。

活跃性问题

所谓活跃性问题,是指程序没法执行下去。

比如说,公司有一个转账的业务,你已经实现了线程安全,解决了安全性问题,并发再高也不出错。那这样是不是完全没问题了呢?

当然不是,程序还会出现活跃性问题,包括:死锁、饥饿、活锁。

其中,活锁是难度最低的一个问题,解决起来非常容易。而且,即使你不解决,活锁也很可能自动解开,完全不用担心。当然,如果你对活锁感兴趣,可以看这篇文章:Java并发编程-活锁:它是那种很少见,又没啥危险的Bug,这里就不多说了,我们得抓重点。

死锁、饥饿是活跃性问题的关键,只要有办法解决这两个问题,就能打通第二关了。

死锁,是指两个以上的线程在执行的时候,因为竞争资源造成互相等待,从而进入“永久”阻塞的状态。这听起来有点拗口,我们还是直接看代码:

class Account {
    // 余额
    private Integer balance;

    // 转账
    void transfer(Account target, Integer amt) {
        synchronized (this) {
            synchronized (target) {
                if (this.balance > amt) {
                    this.balance -= amt;
                    target.balance += amt;
                }
            }
        }
    }
}

上面是一段转账的代码,假设现在同时有两笔交易,账户A账户B账户B账户A,这就有了线程一、线程二。那么,问题来了,线程一锁定了账户A,线程二锁定了账户B,它们都需要对方的资源才能执行,可资源已经被锁定了,只有执行完程序才能释放。

这样一来,线程一、线程二都只能死死等着,永远没法执行。这就是经典的死锁问题,你可以看下面的图:

死锁的资源分布

在这副图中,两个线程形成一个完美的闭环,根本没法出去。你可以看下这篇文章:Java并发编程-死锁(上),里面从头到尾,写了死锁产生的过程。

既然如此,死锁问题该怎么解决呢?除了重启应用外,死锁没法解决,唯一可行的办法是:规避死锁,不让死锁出现。至于怎么规避,你可以看这篇文章:Java并发编程-死锁(下),里面有规避死锁的思路。

饥饿,就是线程拿不到需要的资源,一直没法执行。比如说,下面这段代码:

class Account {
    // 余额
    private Integer balance;

    // 转账
    void transfer(Account target, Integer amt) {
        synchronized (Account.class) {
            // 本系统操作:修改余额,花费 0.01 秒
            if (this.balance > amt) {
                this.balance -= amt;
                target.balance += amt;
            }
            // 调用外部系统:转账,花费 5 秒
            payService.transfer(this, target, amt);
        }
    }
}

这是一段转账的代码,我们如果想要执行转账,那么必须锁定 Account.class。然而,Account.class 由 Java 虚拟机创建,只有一个。这就意味着,所有的转账交易都是串行的,只能一笔一笔的处理,效率极低。

此外,payService.transfer(this, target, amt) 这行代码实在是浪费时间,无论电脑配置多好,速度也完全没法提升。

最致命的是,完成时间没法确定。synchronized 是非公平锁,处理顺序是随机的,可能等待时间短的交易反而先处理,等待时间长的一直不处理。

这就导致,一旦业务量大了,公司的投诉电话很可能被打爆。不过,幸运的是,虽然转账很慢,但程序本身没有问题,只是资源太少,一直没机会运行。

那么,该怎么解决饥饿问题呢?

你可以看看这篇文章:Java并发编程-饥饿,里面讲到了缓解饥饿的三个思路。

并发编程有 3 个关卡:安全性问题、活跃性问题、性能问题,我们今天过的是第二关:活跃性问题。

从这一关开始,我们要特别注意:死锁、饥饿。你可以回顾一下这些文章:Java并发编程-死锁(上)Java并发编程-死锁(下)Java并发编程-饥饿


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK