共计 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 的时候,线程二也会期待一个随机的工夫。
这样一来,因为持有锁的工夫是随机的,反复的概率很小,两个线程的加解锁操作就错开了,转账也就实现了。
写在最初
如果线程互相谦让资源,程序有可能呈现活锁问题。
不过,你也不必太放心。活锁十分少见,即便呈现了,程序也会主动解开。
当然,你也能够在解锁之前,期待一个随机工夫,这样就能防止活锁的呈现。