有时候,线程获得锁进入临界区,却发现在满足某一条件后它才能执行。所以我们要用一个条件对象来管理那些已经获得了一个锁,但是却不能做有用工作的线程。
在之前的模拟银行转账的例子中,假如有一个转账线程进入了临界区,却发现转出账户的余额不足以转出足够的金额,这时候它应该等待其他线程向该账户转入足够的金额。
但是,该线程又获得了锁对象banklock,其他线程无法再进行转账操作。这就是需要条件对象的原因。
Condition对象
java.util.concurrent.locks包中还有一个Condition接口,它将Object监视器方法(wait、notify和notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待set(wait-set)。其中,Lock 替代了synchronized方法和语句的使用,Condition替代了Object监视器方法的使用。
Condition实例实质上被绑定到一个锁上。要为特定Lock实例获得Condition 实例,请使用其newCondition()方法。 一个条件对象可以有一个或多个相关的条件对象。
在模拟银行转账的例子中,我们可以通过语句:private Condition sufficientFunds = banklock.newCondition();
来获得banklock的一个条件对象。
await方法
一个线程进入临界区,如果条件不满足,则应调用await()方法阻塞该线程,并放弃锁。当前线程在接到信号或被中断之前一直处于阻塞状态。
等待获得锁的线程和调用await方法的线程存在本质上的不同。一旦一个线程调用await方法。它进入该条件的等待集。当锁可用时,该线程不能马上解除阻塞。相反,它处于阻塞状态,直到另一个线程调用同一条件上的signalAll方法时为止。
注意:当一个线程调用await时,它无法重新激活自身,只能通过其他线程来激活。如果没有其他线程来重新激活阻塞的线程,它就永远不会再运行了。这就导致了死锁现象。
signalAll方法
当另外一个线程执行转账操作时,它应该调用signalAll方法重新激活因为这一条件而等待的所有线程。
当这些线程从等待集中移出时,它们再次成为可运行的,调度器将再次调度它们。同时,它们将试图重新进入该对象。一旦锁成为可用的,它们当中地某个将从await调用返回,获得该锁,并从被阻塞的地方继续执行。
注意:调用signalAll方法不会立即激活一个阻塞的线程。它仅仅是解除线程的阻塞,以便这些线程可以在当前线程退出同步方法之后,通过竞争实现对对象的访问。
此时,线程应该再次测试该条件。signalAll方法仅仅是通知正在阻塞的线程:此时有可能已经满足条件,值得再次去检测该条件。signalAll方法无法确保该条件被满足。
注意:我们应该在对象的状态有利于等待线程的方向改变时调用signalAll方法。
signal方法
Signal方法是随机解除等待集中某个线程的阻塞状态,但这种方式存在造成死锁的危险。
如果随机选择的线程发现自己任然不能运行,那么它再次被阻塞,如果没有其他线程再次调用signal方法,那么就造成了死锁。
使用方式
class X {
private final ReentrantLock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
// ...
public void m() {
lock.lock(); // block until condition holds
try {
while(!(ok to proceed))
Condition.await();
// ... method body
Condition.signalAll();
} finally {
lock.unlock()
}
}
}
条件对象的使用流程示意图为:
我们可以将之前模拟银行转账的例子中的Bank类的transfer方法做如下修改来使用条件对象:
public boolean transfer(Account fromAccount,Account toAccount,double money) throws InterruptedException {
banklock.lock();
try {
while (fromAccount.getMoney() < money) {
sufficientFunds.await();
}
fromAccount.setMoney(fromAccount.getMoney() - money);
toAccount.setMoney(toAccount.getMoney() + money);
System.out.println(fromAccount.getName() + "向" + toAccount.getName() + "转账" + money + "元");
sufficientFunds.signalAll();
return true;
}
finally {
banklock.unlock();
}
}
总结
1、锁用来保护代码片段(临界区),任何时刻只能有一个线程执行被保护的代码;
2、锁可以管理试图进入被保护代码段的线程;
3、锁可以拥有一个或多个相关的条件对象;
4、每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程。