并发与同步(中)
Java核心技术卷读书笔记
条件对象
-
通常, 线程进入临界区,却发现在某一条件满足之后它才能执行。要使用一个条件对象来管理那些已经获得了一个锁但是却不能做有用工作的线程。
-
现在来细化银行的模拟程序。我们避免选择没有足够资金的账户作为转出账户。注意不 能使用下面这样的代码:
if (bank.getBalance(fron) >= amount {
bank.transfer(from, to, amount);
}
当前线程完全有可能在成功地完成测试,且在调用 transfer 方法之前将被中断。
if (bank.getBalance(from) >= amount)
// thread night be deactivated at this point
bank.transfer(from, to, amount):
在线程再次运行前,账户余额可能已经低于提款金额。必须确保没有其他线程在本检査余额 与转账活动之间修改余额。通过使用锁来保护检査与转账动作来做到这一点。
public void transfer(int from, int to, int amount) {
// 加锁
bankLock.1ock();
try {
while (accounts[from] < amount) {
// 等待
}
// 转移
finally {
// 解锁
bankLock.unlock();
}
}
}
现在,当账户中没有足够的余额时,它会等待直到另一个线程向账户中注入资金。但是这一线程有锁,因此别的线程没有进行存款操作的机会。
对锁对象调用newCondition()
方法可以获得一个Condition
对象。习惯上给每一个条件对象命名为可以反映它所表达的条件的名字。
class Bank {
private Condition sufficientFunds;
public Bank() {
sufficientFunds = bankLock.newCondition();
}
}
如果 transfer 方法发现余额不足,它调用 sufficientFunds.await()
来阻塞当前线程,并使当前线程放弃锁。
等待获得锁的线程和调用 await
方法的线程虽然此时都在等待,但是存在本质上的不同。一旦一个线程调用 await
方法, 它进人该条件的等待集。当锁可用时,该线程不能马上解除阻塞。相反,它处于阻塞状态,直到另一个线程调用同一条件上的 signalAll
方法时为止。
这一调用重新激活因为这一条件而等待的所有线程。当这些线程从等待集当中移出时, 它们再次成为可运行的,调度器将再次激活它们。同时, 它们将试图重新进人该对象。一旦锁成为可用的,它们中的某个将从 await
调用返回, 获得该锁并从被阻塞的地方继续执行。
此时,线程应该再次测试该条件。 由于无法确保该条件被满足—signalAll
方法仅仅是 通知正在等待的线程:此时有可能已经满足条件, 值得再次去检测该条件。
通常, 对 await 的调用应该在如下形式的循环体中
// 如果条件不满足
while ( !(ok to proceed) )
// 一直等待
condition.await();
至关重要的是最终需要某个其他线程调用 signalAll
方法。当一个线程调用await
时,它 没有办法重新激活自身。它寄希望于其他线程。如果没有其他线程来重新激活等待的线程, 它就永远不再运行了。这将导致令人不快的死锁( deadlock) 现象。如果所有其他线程被阻塞,最后一个活动线程在解除其他线程的阻塞状态之前就调用 await 方法, 那么它也被阻塞。 没有任何线程可以解除其他线程的阻塞,那么该程序就挂起了。
经验上讲, 在对象的状态有利于等待线程的方向改变时调用` signalAll`。
public void transfer(int from, int to, int amount) {
bankLock.lock();
try {
// 当小于时,一直等待
while (accounts[from] < amount)
sufficientFunds.await();
// 进行转账部分
// 解除因为accounts[from] < amount的其他线程阻塞
sufficientFunds.signalAll();
}
finally
{
bankLock.unlock();
}
}
注意调用 signalAll
不会立即激活一个等待线程。它仅仅解除等待线程的阻塞, 以便这些 线程可以在当前线程退出同步方法之后,通过竞争实现对对象的访问。
另一个方法 signal
, 则是随机解除等待集中某个线程的阻塞状态。这比解除所有线程的阻塞更加有效,但也存在危险。如果随机选择的线程发现自己仍然不能运行, 那么它再次被 阻塞。如果没有其他线程再次调用 signal
, 那么系统就死锁了。
警告: 当一个线程拥有某个条件的锁时, 它仅仅可以在该条件上调用 await
、signalAll
或 signal
方法。
synchronized 关键字
-
有关锁和条件的关键之处:
- 锁用来保护代码片段, 任何时刻只能有一个线程执行被保护的代码。
- 锁可以管理试图进入被保护代码段的线程。
- 锁可以拥有一个或多个相关的条件对象。
- 每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程。
-
从 1.0 版开始,Java 中的每一个对象都有一个内部锁。如果一个方法用
synchronized
关键字声明,那么对象的锁 将保护整个方法。也就是说,要调用该方法,线程必须获得内部的对象锁。public synchronized void method() { method body } // 等价于 public void method() { this.intrinsicLock.1ock(); try { method body } finally { this.intrinsicLock.unlock(); } }
内部对象锁只有一个相关条件。wait 方法添加一个线程到等待集中,
notifyAll /notify
方法解除等待线程的阻塞状态。换句话说,调用wait
或notityAll
等价于intrinsicCondition.await();intrinsicCondition.signalAll();
-
将静态方法声明为
synchronized
也是合法的。如果调用这种方法,该方法获得相关的类对象的内部锁。如果调用被修饰的静态方法,则此类对象的锁被锁住 ,没有其他线程可以调用同一个类的这个或任何其他的同步静态方法。
简单总结
内部锁和条件存在一些局限。包括:
- 不能中断一个正在试图获得锁的线程。
- 试图获得锁时不能设定超时。
- 每个锁仅有单一的条件, 可能是不够的。 在代码中应该使用哪一种?
Lock
和Condition
对象还是同步方法?下面是一些建议:- 最好既不使用
Lock/Condition
也不使用synchronized
关键字。在许多情况下你可以使 用java.util.concurrent
包中的一种机制,它会为你处理所有的加锁。你也可以使用阻塞队列来同步完成一个共同任务的线程。还应当研究一下并行流。 - 如果
synchronized
关键字适合你的程序, 那么请尽量使用它,这样可以减少编写的代 码数量,减少出错的几率。 - 如果特别需要
Lock/Condition
结构提供的独有特性时,才使用Lock/Condition
。
- 最好既不使用