前言:实际应用中,经常会遇到多个线程共享对同一数据的存取。如果有多个线程存取相同的对象,并且每个线程都调用了一个修改该对象状态的方法,这样的情况,很可能会产生讹误的对象。这种情况通常称为竞争条件(race condition)。
1、锁对象:有两种机制防止代码块受并发访问的干扰。Java语言提供一个synchronized关键字,并且Java SE 5.0引入了ReentrantLock类。
1.1、synchronized关键字:如果一个方法用synchronized关键字声明,那么对象的锁将保护整个方法。就是说,要调用该方法,线程必须获得内部的对象锁。如下:
public synchronized void test(){
}
等价于
public void test(){
内部锁.lock();//“内部锁”只是一个代称,每个对象都有一个内部锁
try{
}finally {
内部锁.unlock();
}
}
(1)内部对象锁只有一个相关条件。wait方法添加一个线程到等待集中,notifyAll/notify方法解除等待线程的阻塞状态。
(2)每个对象都有一个内部锁,且该锁有一个内部条件。由锁来管理那些试图进入synchronized方法的线程,由条件来管理那些调用wait的线程。
(3)可以将静态方法声明为synchronized,该方法获得相关的类对象的内部锁。
(4)内部锁和条件存在一些局限:
(4.1)不能中断一个正在试图获得锁的线程;
(4.2)试图获得锁时不能设定超时;
(4.3)每个锁仅有单一的条件,可能是不够的。
1.2、ReentrantLock类:使用ReentrantLock保护代码块的基本结构如下:
ReentrantLock reentrantLock = new ReentrantLock();
reentrantLock.lock();
try {
critical section(临界区)
} finally {
reentrantLock.unlock();
}
上述结构确保任何时刻只有一个线程进入临界区。一旦一个线程封锁了锁对象,其他任何线程都无法通过lock语句。当其他线程调用lock时,它们被阻塞,直到锁对象被释放。
1.2.1、ReentrantLock(boolean fair):构建一个带有公平策略的锁。一个公平锁偏爱等待时间最长的线程,但这一公平保证会大大降低性能。因此,默认情况下,锁没有被强制公平。
注意:
(1)finally子句中的解锁操作是至关重要的。如果临界区的代码抛出异常,锁必须被释放,否则其他调用锁的线程将永远阻塞。
(2)如果使用锁,就不能使用带资源的try语句。
(3)如果两个线程试图访问同一个对象的锁对象,那么锁以串行方式提供服务。但如果两个线程访问不同对象的锁对象,那两个线程都不会阻塞。
(4)锁是可重入的,因为线程可以重复获得已经持有的锁。锁保持一个持有计数(hold count)来跟踪对lock方法的嵌套调用。
(5)线程在每一次调用lock都要调用unlock来释放锁。由于这一特性,被一个锁保护的代码可以调用另一个使用相同的锁的方法。
(6)要留意不要因为异常的抛出而跳出临界区,如果在代码结束之前抛出异常,finally子句将释放锁,这可能会使对象处于一种受损状态。
1.3、条件对象(条件变量(conditional variable)):有时,线程进入临界区后却发现要满足某一条件后才能执行。要使用一个条件对象来管理那些已经获得了一个锁但却不能做有用工作的线程。
1.3.1、一个锁对象可以有一个或多个相关的条件对象,可以用newCondition()方法获得一个条件对象。
ReentrantLock reentrantLock = new ReentrantLock();
Condition condition = reentrantLock.newCondition();
当发现条件不满足时,可以调用await()方法,然后线程被阻塞,并放弃了锁。
condition.await();
1.3.2、等待获得锁的线程和调用await方法的线程存在本质上的不同。
(1)一旦一个线程调用await方法,它进入该条件的等待集。当锁可用时,该线程不能马上解除阻塞。相反,它处于阻塞状态,直到另一个线程调用同一条件上的signalAll方法为止。
(2)死锁(deadlock):当某个线程调用await时,它无法重新激活自身,只能等待其他某个线程调用signalAll方法。如果没有,它将永远无法再运行。这将导致死锁(deadlock)现象。
(3)调用signalAll不会立即激活一个等待线程,它仅仅解除等待线程的阻塞,以便这些线程可以在当前线程退出同步方法后,通过竞争实现对对象的访问。
2、建议:
(1)最好既不使用Lock/Condition也不使用synchronized,许多情况下可以使用java.util.concurrent包的一种机制来处理所有的加锁。
(2)如果synchronized关键字适合,尽量使用它,这样可以减少编写的代码数量,减少出错的机率。