1.条件对象:
通常,线程进入临界区,却发现在某一条件满足之后它才能执行。要使用一个条件对象来管理那些已经获得了一个锁但是却不能做有用工作的线程。
例如之前的银行转账的例子,在一个线程转账时,如果金额不够,需要等其他账户转进来后才能继续,但是这个线程已经被锁住了,那应该怎么办?
因为,一个锁对象可以有一个或多个相关的条件对象。那么,可以使用newCondition方法获得一个条件对象。例如:
class Bank{
private Condition sufficientFunds;
...
public Bank(){
...
sufficientFunds = bankLock.newCondition();
}
}
这个时候,如果transfer方法发现余额不足,那么就调用:
sufficientFunds.await();
当前线程现在被阻塞,并放弃了锁。
等待获得锁的线程和调用await方法的线程存在本质上的不同。一旦调用了await方法,线程就进入了等待集。当锁可用时,该线程不能马上解除阻塞。相反,它处于阻塞状态,直到另一个线程调用同一条件上的signalAll方法时为止。这个方法会重新激活因为这个条件而等待的所有线程。还有一个方法signal,则是随机解除等待集中某个线程的阻塞状态。
2.synchronized:
总结一下有关锁和条件的关键之处:
1)锁用来保护代码片段,任何时刻只能有一个线程执行被保护的代码。
2)锁可以管理试图进入被保护代码段的线程。
3)锁可以拥有一个或多个相关的条件对象。
4)每个条件对象管理那些已经进入被保护的代码段但不能运行的线程。
从1.0版开始,Java中的每一个对象都有一个内部锁。如果一个方法用synchronized关键字声明,那么对象的锁将保护整个方法。也就是说,要调用该方法,线程必须获得内部的对象锁。
例如:
public synchronized void method(){}
等价于:
public void method(){
this.intrinsicLock.lock();
try{
method body
}
finally{
this.intrinsicLock.unlock();
}
}
内部锁对象只有一个相关条件。wait方法添加一个线程到等待集中,notifyAll/notify方法解除等待线程的阻塞状态。
换句话说,调用wait或notifyAll等价于:
inrtrinsicCondition.await();
inrtrinsicCondition.singlAll();
例如用Java实现之前的Bank类:
class Bank{
private double[] accounts;
public synchronized void transfer(int from, int to, int amount) throw InterruptedException{
while(account[from] < amount)
wait();
account[from] -= amount;
account[to] += amount;
notifyAll();
}
public synchronized double getTotalBalance(){...}
}
将静态方法声明为synchronized也是合法的。如果调用该方法,该方法获得相关的类对象的内部锁。例如Bank类有一个静态同步方法,那么当该方法被调用时,Bank.class对象的锁被锁住。因此,没有其他线程可以调用同一个类的这个或任何其他的同步静态方法。
但是内部锁和条件有一些局限:
1)不能中断一个正在试图获得锁的线程。
2)试图获得锁时不能设定超时。
3)每个锁仅有单一的条件,可能不够。
最好上面提到的两种方法都不要不是,我们更多的会使用后面讲到的java.util.concurrent包中的一种机制。
3.同步阻塞:
还有一种机制可以获得锁,通过进入一个同步阻塞。当线程进入如下形式的阻塞:
synchronized(obj){
...
}
于是,它会获得obj的锁。
使用一个对象的锁来实现额外的原子操作,实际上称为客户端锁定。
例如使用Vector类(它的方法是同步的)来操作之前的transfer方法:
public void transfer(Vector<Double> accounts, int from, int to, int amount){
accounts.set(from, accounts.get(from) - amount);
accounts.set(to, accounts.get(to) + amount);
System.out.println(...);
}
虽然Vector类的get和set方法是同步的,但是这并没有什么帮助。在第一次对get调用已经完成之后,一个线程完全有可能在transfer方法中剥夺运行权。于是,另一个线程可能在相同的存储位置存入不同的值。但是可是截获这个锁:
public void transfer(Vector<Double> accounts, int from, int to, int amount){
synchronized(accounts){
accounts.set(from, accounts.get(from) - amount);
accounts.set(to, accounts.get(to) + amount);
}
System.out.println(...);
}
这个方法可以工作,但是它完全依赖于Vector类对自己的所有可修改方法都使用内部锁。所以,客户端锁定是非常脆弱的,通常也不推荐使用。