在前面的两篇文章中,我们一直在关注程序的死锁问题,包括:造成死锁的原因、规避死锁的办法。
然而,除了死锁的问题外,还有另外两种情况。它们虽然没那么常见,可一旦发生,程序照样无法执行下去。
这一次,我们先看看其中一种情况—活锁。
线程的相互谦让—活锁
通过前面两篇文章,相信你已经知道了:发生死锁后,线程会相互等待,进入一个“永久”堵塞的状态。
归根到底,就是几个线程占有了资源,又没资格运行程序,也不肯释放手上的资源,结果谁都没法运行。
那如果线程让出资源,问题是不是就解决了呢?
当然不是,虽然线程让出了资源,没进入阻塞状态,但依然执行不下去,这就是活锁。
所谓活锁,就是两个以上的线程在执行的时候,因为相互谦让资源,结果都拿不到资源,没法运行程序。
打个比方,你我在路上迎面碰到,那咱俩如果想过去,就相互谦让呀。结果,你从右边走,我从左边走,又撞上了。就像下面这样:
在现实世界中,人们会相互交流,一般谦让几次也就过去了。
但在编程世界中,几个线程可能会一直谦让下去,成为没有阻塞但依然没法执行的活锁。
活锁带来的问题
说实话,在日常工作中,活锁很少出现。而且,就算出现了活锁,但还没等你采取行动,它就自动解开了。而这会导致一些很奇葩的问题。
比如说,业务量明明不算大,但转账却好几分钟都没完成。可当你开始检查,正一头雾水的时候,转账忽然就完成了。你看这段代码:
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的时候,线程二也会等待一个随机的时间。
这样一来,由于持有锁的时间是随机的,重复的概率很小,两个线程的加解锁操作就错开了,转账也就完成了。
写在最后
如果线程相互谦让资源,程序有可能出现活锁问题。
不过,你也不用太担心。活锁非常少见,即使出现了,程序也会自动解开。
当然,你也可以在解锁之前,等待一个随机时间,这样就能避免活锁的出现。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。