关于java:Java并发编程活锁一个不怎么出现危害很小的Bug

5次阅读

共计 2053 个字符,预计需要花费 6 分钟才能阅读完成。

在后面的两篇文章中,咱们始终在关注程序的死锁问题,包含:造成死锁的起因、躲避死锁的方法。

然而,除了死锁的问题外,还有另外两种状况。它们尽管没那么常见,可一旦产生,程序照样无奈执行上来。

这一次,咱们先看看其中一种状况—活锁。

线程的互相谦让—活锁

通过后面两篇文章,置信你曾经晓得了:产生死锁后,线程会互相期待,进入一个“永恒”梗塞的状态。

归根到底,就是几个线程占有了资源,又没资格运行程序,也不肯开释手上的资源,后果谁都没法运行。

那如果线程让出资源,问题是不是就解决了呢?

当然不是,尽管线程让出了资源,没进入阻塞状态,但仍然执行不上来,这就是 活锁

所谓活锁,就是两个以上的线程在执行的时候,因为互相谦让资源,后果都拿不到资源,没法运行程序。

打个比方,你我在路上迎面碰到,那咱俩如果想过来,就互相谦让呀。后果,你从左边走,我从右边走,又撞上了。就像上面这样:

在事实世界中,人们会互相交换,个别谦让几次也就过来了。

但在编程世界中,几个线程可能会始终谦让上来,成为没有阻塞但仍然没法执行的 活锁

活锁带来的问题

说实话,在日常工作中,活锁很少呈现。而且,就算呈现了活锁,但还没等你采取行动,它就主动解开了。而这会导致一些很奇葩的问题。

比如说,业务量明明不算大,但转账却好几分钟都没实现。可当你开始查看,正一头雾水的时候,转账突然就实现了。你看这段代码:

class Account {
    private int balance;
    private final Lock lock = new ReentrantLock();

    // 转账
    // 上面的代码是简化版,千万别模拟,解锁肯定要用 try-finally 包裹
    void transfer(Account tar, int amt) {
        boolean flag = true;

        // 一直尝试加锁两个账户
        while (flag) {if(this.lock.tryLock()) {if (tar.lock.tryLock()) {
                    // 加锁胜利,执行转账
                    this.balance -= amt;
                    tar.balance += amt;

                    // 跳出循环
                    flag = false;
                }
            }

            // 开释锁资源
            tar.lock.unlock();
            this.lock.unlock();}
    }
}

在这个例子中,Lock 是 Java 实现互斥的接口,它补救了 synchronized 的一些缺点。

比方,应用了 Lock后,如果线程没拿到锁,那并不会进入阻塞状态,而是间接退出。这样一来,线程就能开释出占用的资源,从而毁坏了不可抢占条件,防止死锁的产生。

然而,新问题又来了。

当初同时呈现两笔交易,账户 A 转账户 B,账户 B 转账户 A。那么,线程一会占有账户 A,线程二则会占有账户 B。

后果,线程一拿不到账号 B,转账没法执行,线程一完结;线程二也拿不到账号 A,转账也没法执行,线程二完结。

到这里,第一轮循环完结,两笔转账都没有实现,新一轮循环开启。但在第二轮循环中,下面的过程又再次反复。如此一直地循环上来,两笔转账却始终没有实现。

简略来说,程序呈现了 活锁问题

活锁问题一旦呈现,你要不就重启利用,要不就等问题主动隐没,可这些都是十分消极的做法。

所以,要想彻底解决问题,咱们还是得防止活锁,不让活锁产生。

如何防止活锁

程序之所以呈现活锁,其实是因为线程的解锁工夫是一样的。

比如说,两个线程如果同时解锁,重试时同时加锁,这个过程又一直循环,可不就是陷入死循环吗?

因而,想要防止活锁产生,咱们能够在解锁之前,期待一个随机工夫。因为每个线程的解锁工夫都不一样,也就不存在始终让资源的状况。

比方,下面的转账业务,咱们能够批改两行代码。

class Account {
    private int balance;
    private final Lock lock = new ReentrantLock();

    // 转账
    // 上面的代码是简化版,千万别模拟,解锁肯定要用 try-finally 包裹
    void transfer(Account tar, int amt) {
        boolean flag = true;

        // 一直尝试加锁两个账户
        while (flag) {if(this.lock.tryLock(随机数, TimeUnit.MILLISECONDS)) {if (tar.lock.tryLock(随机数, TimeUnit.MILLISECONDS)) {
                    // 加锁胜利,执行转账
                    this.balance -= amt;
                    tar.balance += amt;

                    // 跳出循环
                    flag = false;
                }
            }

            // 开释锁资源
            tar.lock.unlock();
            this.lock.unlock();}
    }
}

在锁定账户 A 的时候,线程一会先期待一个随机的工夫;在锁定账户 B 的时候,线程二也会期待一个随机的工夫。

这样一来,因为持有锁的工夫是随机的,反复的概率很小,两个线程的加解锁操作就错开了,转账也就实现了。

写在最初

如果线程互相谦让资源,程序有可能呈现活锁问题。

不过,你也不必太放心。活锁十分少见,即便呈现了,程序也会主动解开。

当然,你也能够在解锁之前,期待一个随机工夫,这样就能防止活锁的呈现。

正文完
 0