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

并发编程有 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并发编程-饥饿

你可能感兴趣的:(java后端并发并发编程)