在大多数实际的多线程应用中,两个或两个以上的线程需要共享对同一数据的存取。如果两个线程存取相同的对象,并且每个线程都调用了一个修改该对象状态的方法,可能会产生错误。这样的一个情况通常称为竞争条件。
为了避免多线程引起的对共享数据的讹误,必须学习如何同步存取。
有两种机制防止代码块受到并发访问的干扰。Java语言提供一个synchronized关键字达到这一目的,并且Java SE 5.0引入了ReentrantLock类。synchronized关键字自动提供一个锁以及相关的“条件”,对于大多数需要显示锁的情况,这是很便利的。
1.Lock/Condition对象
锁对象
使用ReentrantLock保护代码块的基本结构如下,这一结构确保任何时刻只有一个线程进入临界区。一旦一个线程封锁了锁对象,其他任何线程都无法通过lock语句。当其他线程调用lock时,它们被阻塞,直到第一个线程释放锁对象。
//ReentrantLock对象
myLock.lock();
try{
...
}
finally {
myLock.unLock();
}
锁是可重入的,因为线程可以重复地获得已经持有的锁。锁保持一个持有计数来跟踪对lock方法的嵌套调用。线程在每一次调用lock都要调用unlock来释放锁。由于这一特性,被一个锁保护的代码可以调用另一个使用相同的锁的方法。
条件对象
通常,线程进入临界区,却发现在某一条件满足之后它才能执行。要使用一个条件对象来管理那些已经获得了一个锁但是却不能做有用工作的线程。
一个锁对象可以有一个或多个相关的条件对象。可以用newCondition方法获得一个条件对象。
以下用银行转账的例子演示条件对象:
private Condition sufficientFunds = bankLock.newCondition();
//转账的方法实现
//from 付款账户
//to 收款账户
//amount 转账金额
public void transfer(int from, int to, int amount) {
bankLock.lock();//获得锁对象
try{
while (accounts[from] < amount)//付款账户余额小于转账金额
{
//等待,调用await方法
sufficientFunds.await();
}
//进行转账操作
...
sufficientFunds.signalAll();
}
finally {
bankLock.unlock();
}
}
如果transfer方法发现余额不足,调用sufficientFunds.await()。当前线程被阻塞,并放弃锁。此时希望另一个线程可以进行增加账户余额的操作。
等待获得锁的线程和调用await方法的线程存在本质上的不同。一旦一个线程调用await方法,它进入该条件的等待集。当锁可用时,该线程不能马上解除阻塞。相反,它处于阻塞状态,直到另一个线程调用同一条件上的signalAll方法时为止。
当另一个线程成功转账时,应该调用sufficientFunds.signalAll()。这一调用重新激活因为这一条件而等待的所有线程。
当等待线程从等待集中移出,调度器将再次激活它们,同时它们将试图重新进入该对象。一旦锁成为可用的,它们中的某个将从await调用返回,获得该锁并从被阻塞的地方继续执行。
此时,线程应该再次测试该条件。由于无法确保该条件被满足----signalAll方法仅仅时通知正在等待的线程:此时有可能已经满足条件,值得再次去检测该条件。
当一个线程调用await时,它没有办法重新激活自身。它寄希望于其他线程,如果没有其他线程调用signalAll来重新激活等待的线程,它就永远不再运行了。
注意调用signalAll不会立即激活一个等待线程。它仅仅解除等待线程的阻塞,以便这些线程可以再当前线程退出同步方法之后,通过竞争实现对对象的访问。
另一个方法signal,则是随机解除等待集中某个线程的阻塞状态。
2.synchronized关键字
synchronized方法
Lock和Condition接口为程序设计人员提供了高度的锁定控制。然而,大多数情况下,不需要那样的控制,并且可以使用一种嵌入到Java语言内部的机制。从1.0版开始,Java中的每一个对象都有一个内部锁。如果一个方法用synchronized关键字声明,那么对象的锁将保护整个方法。
也就是说:
public synchronized void method() {
method body
}
等价于
public void method() {
this.intrinsicLock.lock();
try {
method body
}
finally{ this.intrinsicLock.unlock();}
}
内部对象锁只有一个相关条件。wait方法添加一个线程到等待集中,notifyAll/notify方法解除等待线程的阻塞状态。也就是说,调用wait或notifyAll等价于await和signallAll。
将静态方法声明为synchronized也是合法的。如果调用这种方法,该方法获得相关的类对象的内部锁。例如,如果Bank类有一个静态同步方法,那么当该方法被调用时,Bank.class对象的锁被锁住。因此没有其他线程可以调用同一个类的这个或任何其他的静态同步方法。
synchronized块
也存在另一种使用synchronized关键字的方式,即synchronized()-----同步块。
public synchronized void methodA() {
System.out.println("methodA.....");
}
public void methodB() {
synchronized(this) {
System.out.pritntln("methodB.....");
}
}
public void methodC() {
String str = "sss";
synchronized (str) {
System.out.println("methodC.....");
}
methodA()和methodB()用的是同一个锁,因为methodB()的this对象,代表的就是方法调用的对象,所以A和B是同一把锁。而C的锁则是str对象的内部锁。
3.总结
应该使用哪种?是Lock/Condition对象还是同步方法?
- 最好既不使用Lock/Condition也不使用synchronized关键字。在许多情况下可以使用java.util.concurrent包中的一种机制,它会为你处理所有的加锁。
- 如果synchronized关键字适合自己的程序,请尽量使用,这样可以减少编写的代码数量,减少出错的机率。
- 如果特别需要Lock/Condition结构提供的独有特性是,才使用Lock/Condition。
网友评论