- Lock接口
Lock
实现提供了比使用synchronized
方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的Condition
对象。
锁是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问。一次只能有一个线程获得锁,对共享资源的所有访问都需要首先获得锁。不过,某些锁可能允许对共享资源并发访问,如 ReadWriteLock
的读取锁。
synchronized
方法或语句的使用提供了对与每个对象相关的隐式监视器锁的访问,但却强制所有锁获取和释放均要出现在一个块结构中:当获取了多个锁时,它们必须以相反的顺序释放,且必须在与所有锁被获取时相同的词法范围内释放所有锁。
虽然 synchronized
方法和语句的范围机制使得使用监视器锁编程方便了很多,而且还帮助避免了很多涉及到锁的常见编程错误,但有时也需要以更为灵活的方式使用锁。例如,某些遍历并发访问的数据结果的算法要求使用 "hand-over-hand" 或 "chain locking":获取节点 A 的锁,然后再获取节点 B 的锁,然后释放 A 并获取 C,然后释放 B 并获取 D,依此类推。Lock
接口的实现允许锁在不同的作用范围内获取和释放,并允许以任何顺序获取和释放多个锁,从而支持使用这种技术。
随着灵活性的增加,也带来了更多的责任。不使用块结构锁就失去了使用 synchronized
方法和语句时会出现的锁自动释放功能。在大多数情况下,应该使用以下语句:
Lock l = ...;
l.lock();
try {
// access the resource protected by this lock
} finally {
l.unlock();
}
锁定和取消锁定出现在不同作用范围中时,必须谨慎地确保保持锁定时所执行的所有代码用 try-finally 或 try-catch 加以保护,以确保在必要时释放锁。
Lock
实现提供了使用 synchronized
方法和语句所没有的其他功能,包括提供了一个非块结构的获取锁尝试 tryLock()
、一个获取可中断锁的尝试 lockInterruptibly()
和一个获取超时失效锁的尝试tryLock(long, TimeUnit)
。
Lock
类还可以提供与隐式监视器锁完全不同的行为和语义,如保证排序、非重入用法或死锁检测。如果某个实现提供了这样特殊的语义,则该实现必须对这些语义加以记录。
注意,Lock
实例只是普通的对象,其本身可以在 synchronized
语句中作为目标使用。获取 Lock
实例的监视器锁与调用该实例的任何 lock()
方法没有特别的关系。为了避免混淆,建议除了在其自身的实现中之外,决不要以这种方式使用 Lock
实例。
除非另有说明,否则为任何参数传递 null
值都将导致抛出 NullPointerException
。
内存同步
所有 Lock
实现都必须 实施与内置监视器锁提供的相同内存同步语义,如 The Java Language Specification, Third Edition (17.4 Memory Model) 中所描述的:
- 成功的
lock
操作与成功的 Lock 操作具有同样的内存同步效应。 - 成功的
unlock
操作与成功的 Unlock 操作具有同样的内存同步效应。
不成功的锁定与取消锁定操作以及重入锁定/取消锁定操作都不需要任何内存同步效果。
实现注意事项
三种形式的锁获取(可中断、不可中断和定时)在其性能特征、排序保证或其他实现质量上可能会有所不同。而且,对于给定的 Lock
类,可能没有中断正在进行的 锁获取的能力。因此,并不要求实现为所有三种形式的锁获取定义相同的保证或语义,也不要求其支持中断正在进行的锁获取。实现必需清楚地对每个锁定方法所提供的语义和保证进行记录。还必须遵守此接口中定义的中断语义,以便为锁获取中断提供支持:完全支持中断,或仅在进入方法时支持中断。
由于中断通常意味着取消,而通常又很少进行中断检查,因此,相对于普通方法返回而言,实现可能更喜欢响应某个中断。即使出现在另一个操作后的中断可能会释放线程锁时也是如此。实现应记录此行为。
public interface Lock {
/**
* 获取锁。
* 如果锁不可用,出于线程调度目的,将禁用当前线程,并且在获得锁之前,该线程将一直处于休眠状态。
*
* 实现注意事项:
* Lock 实现可能能够检测到锁的错误使用,比如会导致死锁的调用,在那种环境下还可能抛出一个 (unchecked) 异常。Lock 实现必须对环境和异常类型进行记录。
*/
void lock();
/**
* 如果当前线程未被 中断,则获取锁。
* 如果锁可用,则获取锁,并立即返回。
*
* 如果锁不可用,出于线程调度目的,将禁用当前线程,并且在发生以下两种情况之一以前,该线程将一直处于休眠状态:
*
* 锁由当前线程获得;或者
* 其他某个线程中断当前线程,并且支持对锁获取的中断。
* 如果当前线程:
*
* 在进入此方法时已经设置了该线程的中断状态;或者
* 在获取锁时被中断,并且支持对锁获取的中断,
* 则将抛出 InterruptedException,并清除当前线程的已中断状态。
* 实现注意事项
*
* 在某些实现中可能无法中断锁获取,即使可能,该操作的开销也很大。程序员应该知道可能会发生这种情况。在这种情况下,该实现应该对此进行记录。
*
* 相对于普通方法返回而言,实现可能更喜欢响应某个中断。
*
* Lock 实现可能可以检测锁的错误用法,例如,某个调用可能导致死锁,在特定的环境中可能抛出(未经检查的)异常。该 Lock 实现必须对环境和异常类型进行记录。
*/
void lockInterruptibly() throws InterruptedException;
/**
* 仅在调用时锁为空闲状态才获取该锁。
* 如果锁可用,则获取锁,并立即返回值 true。如果锁不可用,则此方法将立即返回值 false。
*
* 此方法的典型使用语句如下:
*
* Lock lock = ...;
* if (lock.tryLock()) {
* try {
* // manipulate protected state
* } finally {
* lock.unlock();
* }
* } else {
* // perform alternative actions
* }
*
* 此用法可确保如果获取了锁,则会释放锁,如果未获取锁,则不会试图将其释放。
* 返回:
* 如果获取了锁,则返回 true;否则返回 false。
*/
boolean tryLock();
/**
*如果锁在给定的等待时间内空闲,并且当前线程未被 中断,则获取锁。
* 如果锁可用,则此方法将立即返回值 true。如果锁不可用,出于线程调度目的,将禁用当前线程,并且在发生以下三种情况之一前,该线程将一直处于休眠状态:
*
* 锁由当前线程获得;或者
* 其他某个线程中断当前线程,并且支持对锁获取的中断;或者
* 已超过指定的等待时间
* 如果获得了锁,则返回值 true。
*
* 如果当前线程:
*
* 在进入此方法时已经设置了该线程的中断状态;或者
* 在获取锁时被中断,并且支持对锁获取的中断,
* 则将抛出 InterruptedException,并会清除当前线程的已中断状态。
* 如果超过了指定的等待时间,则将返回值 false。如果 time 小于等于 0,该方法将完全不等待。
*
* 实现注意事项
*
* 在某些实现中可能无法中断锁获取,即使可能,该操作的开销也很大。程序员应该知道可能会发生这种情况。在这种情况下,该实现应该对此进行记录。
*
* 相对于普通方法返回而言,实现可能更喜欢响应某个中断,或者报告出现超时情况。
*
* Lock 实现可能可以检测锁的错误用法,例如,某个调用可能导致死锁,在特定的环境中可能抛出(未经检查的)异常。该 Lock 实现必须对环境和异常类型进行记录。
*
* 参数:
* time - 等待锁的最长时间
* unit - time 参数的时间单位
* 返回:
* 如果获得了锁,则返回 true;如果在获取锁前超过了等待时间,则返回 false
* 抛出:
* InterruptedException - 如果在获取锁时,当前线程被中断(并且支持对锁获取的中断)
*/
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
/**
*释放锁。
* 实现注意事项
*
* Lock 实现通常对哪个线程可以释放锁施加了限制(通常只有锁的保持者可以释放它),如果违背了这个限制,可能会抛出(未经检查的)异常。该 Lock 实现必须对所有限制和异常类型进行记录。
*/
void unlock();
/**
*返回绑定到此 Lock 实例的新 Condition 实例。
* 在等待条件前,锁必须由当前线程保持。调用 Condition.await() 将在等待前以原子方式释放锁,并在等待返回前重新获取锁。
*
* 实现注意事项
*
* Condition 实例的具体操作依赖于 Lock 实现,并且该实现必须对此加以记录。
*
* 返回:
* 用于此 Lock 实例的新 Condition 实例
* 抛出:
* UnsupportedOperationException - 如果此 Lock 实现不支持条件
*/
Condition newCondition();
}
-
ReadWriteLock 接口
ReadWriteLock 维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有 writer,读取锁可以由多个 reader 线程同时保持。写入锁是独占的。
所有ReadWriteLock 实现都必须保证 writeLock操作的内存同步效果也要保持与相关readLock的联系。也就是说,成功获取读锁的线程会看到写入锁之前版本所做的所有更新。
与互斥锁相比,读-写锁允许对共享数据进行更高级别的并发访问。虽然一次只有一个线程(writer 线程)可以修改共享数据,但在许多情况下,任何数量的线程可以同时读取共享数据(reader 线程),读-写锁利用了这一点。从理论上讲,与互斥锁相比,使用读-写锁所允许的并发性增强将带来更大的性能提高。在实践中,只有在多处理器上并且只在访问模式适用于共享数据时,才能完全实现并发性增强。
与互斥锁相比,使用读-写锁能否提升性能则取决于读写操作期间读取数据相对于修改数据的频率,以及数据的争用——即在同一时间试图对该数据执行读取或写入操作的线程数。例如,某个最初用数据填充并且之后不经常对其进行修改的 collection,因为经常对其进行搜索(比如搜索某种目录),所以这样的 collection 是使用读-写锁的理想候选者。但是,如果数据更新变得频繁,数据在大部分时间都被独占锁,这时,就算存在并发性增强,也是微不足道的。更进一步地说,如果读取操作所用时间太短,则读-写锁实现(它本身就比互斥锁复杂)的开销将成为主要的执行成本,在许多读-写锁实现仍然通过一小段代码将所有线程序列化时更是如此。最终,只有通过分析和测量,才能确定应用程序是否适合使用读-写锁。
尽管读-写锁的基本操作是直截了当的,但实现仍然必须作出许多决策,这些决策可能会影响给定应用程序中读-写锁的效果。这些策略的例子包括:- 在 writer 释放写入锁时,reader 和 writer 都处于等待状态,在这时要确定是授予读取锁还是授予写入锁。Writer 优先比较普遍,因为预期写入所需的时间较短并且不那么频繁。Reader 优先不太普遍,因为如果 reader 正如预期的那样频繁和持久,那么它将导致对于写入操作来说较长的时延。公平或者“按次序”实现也是有可能的。
- 在 reader 处于活动状态而 writer 处于等待状态时,确定是否向请求读取锁的 reader 授予读取锁。Reader 优先会无限期地延迟 writer,而 writer 优先会减少可能的并发。
- 确定是否重新进入锁:可以使用带有写入锁的线程重新获取它吗?可以在保持写入锁的同时获取读取锁吗?可以重新进入写入锁本身吗?
- 可以将写入锁在不允许其他 writer 干涉的情况下降级为读取锁吗?可以优先于其他等待的 reader 或 writer 将读取锁升级为写入锁吗?
public interface ReadWriteLock { /** 返回用于读的锁 */ Lock readLock(); /**返回用于写的锁 */ Lock writeLock(); }
Condition接口
Condition
将Object
监视器方法(wait
、notify
和notifyAll
)分解成截然不同的对象,以便通过将这些对象与任意Lock
实现组合使用,为每个对象提供多个等待 set(wait-set)。其中,Lock
替代了synchronized
方法和语句的使用,Condition
替代了 Object 监视器方法的使用。
条件(也称为条件队列 或条件变量)为线程提供了一个含义,以便在某个状态条件现在可能为 true 的另一个线程通知它之前,一直挂起该线程(即让其“等待”)。因为访问此共享状态信息发生在不同的线程中,所以它必须受保护,因此要将某种形式的锁与该条件相关联。等待提供一个条件的主要属性是:以原子方式 释放相关的锁,并挂起当前线程,就像 Object.wait
做的那样。
Condition
实例实质上被绑定到一个锁上。要为特定 Lock
实例获得 Condition
实例,请使用其 newCondition()
方法。
作为一个示例,假定有一个绑定的缓冲区,它支持 put
和 take
方法。如果试图在空的缓冲区上执行 take
操作,则在某一个项变得可用之前,线程将一直阻塞;如果试图在满的缓冲区上执行 put
操作,则在有空间变得可用之前,线程将一直阻塞。我们喜欢在单独的等待 set 中保存 put
线程和 take
线程,这样就可以在缓冲区中的项或空间变得可用时利用最佳规划,一次只通知一个线程。可以使用两个 Condition
实例来做到这一点。
class BoundedBuffer {
**final Lock lock = new ReentrantLock();**
final Condition notFull = **lock.newCondition();**
final Condition notEmpty = **lock.newCondition();**
final Object[] items = new Object[100];
int putptr, takeptr, count;
public void put(Object x) throws InterruptedException {
**lock.lock();
try {**
while (count == items.length)
**notFull.await();**
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
**notEmpty.signal();**
**} finally {
lock.unlock();
}**
}
public Object take() throws InterruptedException {
**lock.lock();
try {**
while (count == 0)
**notEmpty.await();**
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
**notFull.signal();**
return x;
**} finally {
lock.unlock();
}**
}
}
( ArrayBlockingQueue
类提供了这项功能,因此没有理由去实现这个示例类。)
Condition
实现可以提供不同于 Object
监视器方法的行为和语义,比如受保证的通知排序,或者在执行通知时不需要保持一个锁。如果某个实现提供了这样特殊的语义,则该实现必须记录这些语义。
注意,Condition
实例只是一些普通的对象,它们自身可以用作 synchronized
语句中的目标,并且可以调用自己的 wait
和 notification
监视器方法。获取 Condition
实例的监视器锁或者使用其监视器方法,与获取和该 Condition
相关的 Lock
或使用其 waiting
和 signalling
方法没有什么特定的关系。为了避免混淆,建议除了在其自身的实现中之外,切勿以这种方式使用 Condition
实例。
除非另行说明,否则为任何参数传递 null
值将导致抛出 NullPointerException
。
实现注意事项
在等待 Condition
时,允许发生“虚假唤醒”,这通常作为对基础平台语义的让步。对于大多数应用程序,这带来的实际影响很小,因为 Condition
应该总是在一个循环中被等待,并测试正被等待的状态声明。某个实现可以随意移除可能的虚假唤醒,但建议应用程序程序员总是假定这些虚假唤醒可能发生,因此总是在一个循环中等待。
三种形式的条件等待(可中断、不可中断和超时)在一些平台上的实现以及它们的性能特征可能会有所不同。尤其是它可能很难提供这些特性和维护特定语义,比如排序保证。更进一步地说,中断线程实际挂起的能力在所有平台上并不是总是可行的。
因此,并不要求某个实现为所有三种形式的等待定义完全相同的保证或语义,也不要求其支持中断线程的实际挂起。
要求实现清楚地记录每个等待方法提供的语义和保证,在某个实现不支持中断线程的挂起时,它必须遵从此接口中定义的中断语义。
由于中断通常意味着取消,而又通常很少进行中断检查,因此实现可以先于普通方法的返回来对中断进行响应。即使出现在另一个操作后的中断可能会释放线程锁时也是如此。实现应记录此行为。
public interface Condition {
// 造成当前线程在接到信号或被[中断](http://tool.oschina.net/uploads/apidocs/jdk- zh/java/lang/Thread.html#interrupt())之前一直处于等待状态。
void await() throws InterruptedException;
// 造成当前线程在接到信号之前一直处于等待状态。
void awaitUninterruptibly();
// 造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
long awaitNanos(long nanosTimeout) throws InterruptedException;
//造成当前线程在接到信号之前一直处于等待状态。
boolean await(long time, TimeUnit unit) throws InterruptedException;
// 造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态。
boolean awaitUntil(Date deadline) throws InterruptedException;
//唤醒一个等待在Condition上的线程
void signal();
//唤醒所有等待在这个Condition上的线程
void signalAll();
}