显式锁
在Java 5.0 之前,在协调对共享对象的访问时可以使用的机制只有synchronized和volatile。Java 5.0增加了一种新的机制:ReentrantLock。与之前提到过的机制相反,ReentrantLock并不是一种替代内置加锁的方法,而是当内置加锁机制不适用时,作为一种可选择的高级功能。
13.1 Lock与ReentrantLock
Lock接口中定义了一种无条件、可轮询的、定时的以及可中断的锁获取操作,所有加锁和解锁的方法都是显式的。在Lock的实现中必须提供与内置锁相同的内存可见性语义,但在加锁语义、调度算法、顺序保证以及性能特性等方面可以有所不同。
public interfece Lock
{
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long timeout, TimeUnit unit
throw InterruptedException;
void unlock();
Condition newCondition();
}
ReentrantLock实现了Lock接口,并提供了与synchronized相同的互斥性和内存可见性。在获取ReentrantLock时,有着与进入同步代码块相同的内存语义,在释放ReentrantLock时,同样有着与退出同步代码块相同的内存语义。此外,与synchronized一样,ReentrantLock还提供了可重入的加锁语义。ReentrantLock支持在Lock接口中定义的所有获取锁模式,并且与synchronized相比,它还为处理锁的不可用性问题提供了更高的灵活性。
为什么要创建一种与内置锁如此现实的加锁机制?在大多数情况下,内置锁能很好地工作,但在功能上存在一些局限性,例如,无法中断一个正在等待获取锁的线程,或者无法再请求一个锁时无限地等待下去。内置锁必须在获取该锁的代码块中释放,这就简化了编码工作,并且与异常处理操作实现了很好地交互,但却无法实现非阻塞结构的加锁规则。这些都是使用synchronized的原因,但在某些情况下,一种更灵活的加锁机制通常能提供更好地活跃性和性能,因为它可以跨方法级来释放。
下面给出了Lock接口的标准使用形式。这种形式比使用内置锁复杂一些:必须在finally块中释放锁。否则,如果在被保护的代码块中抛出了异常,那么这个锁永远都无法释放。当使用加锁时,还必须考虑在try块中抛出异常的情况,如果可能使对象处于不一致的状态,那么就需要更多的try-catch或try-finally代码块。(当使用其他形式的加锁时,包括内置锁,都应该考虑在出现异常时的情况。)
Lock lock = new ReentrantLock();
...
lock.lock();
try {
// 更新对象状态
// 捕获异常,并在必要时恢复不变性条件
} finally {
lock.unlock();//一定要记得在finally块里释放
}
显示锁不像内部锁那样退出块时会自动释放,因为显示锁根本就没有记录锁本应被释放的位置和时间,这就是ReentrantLock不能完全替换synchronized的原因:它更加的“危险”,因为当程序的控制权离开被保护的代码块时,不会自动清除锁。
13.1.1 轮询锁与定时锁
线程在调用lock()方法来获得另一个线程所持有的锁时,会发生阻塞,你应该对这样方式获得锁更加谨慎。而tryLock方法试图获得一个锁,如果成功则返回true,否则立即返回false,并且线程可以立即离开去做其他的事情。
可定时的与可轮询的锁获取模式是由tryLock方法实现,与无条件的锁获取模式相比,它具有更完善的错误恢复机制。在内置锁中,死锁是一个严重的问题,恢复程序的唯一方法是重新启动程序,而防止死锁的唯一方法就是在构造程序时避免出现不一致的锁顺序。可定时的与可轮询的锁提供了另一种选择,避免死锁的发生。
轮询锁(立即锁)—— Lock.tryLock():即使已将此锁定设置为使用公平排序策略,但是调用 tryLock() 仍将立即获取锁定(如果有可用的),而不管其他线程当前是否正在等待该锁定。在某些情况下,此“闯入”行为可能会打破公平性,如果希望遵守此锁定的公平设置,则使用 tryLock(0, TimeUnit.SECONDS) ,它几乎是等效的。
如果不能获得所有需要的锁,那么可以使用可定时的或可轮询的锁获取方式,从而使你重新获得控制权,它会释放已经获得的锁,然后重新尝试获取所有锁(或者至少会将这个失败记录到日志,并采取其他措施)。下面程序使用了另一种方法来解决动态顺序死锁的问题:使用tryLock来获取两个锁,如果不能同时活得那么就回退并重新尝试。在休眠时间中包括固定部分和随机部分,从而降低发生活锁的可能性。如果在指定的时间内不能获得所有需要的锁,那么transferMoney将返回一个失败的状态,从而使该操作平缓地失败。
public class DeadlockAvoidance {
private static Random rnd = new Random();
public boolean transferMoney(Account fromAcct,
Account toAcct,
DollarAmount amount,
long timeout,
TimeUnit unit)
throws InsufficientFundsException, InterruptedException {
long fixedDelay = getFixedDelayComponentNanos(timeout, unit);
long randMod = getRandomDelayModulusNanos(timeout, unit);
long stopTime = System.nanoTime() + unit.toNanos(timeout);
while (true) {
if (fromAcct.lock.tryLock()) {
try {
if (toAcct.lock.tryLock()) {
try {
if (fromAcct.getBalance().compareTo(amount) < 0)
throw new InsufficientFundsException();
else {
fromAcct.debit(amount);
toAcct.credit(amount);
return true;
}
} finally {
toAcct.lock.unlock();
}
}
} finally {
fromAcct.lock.unlock();
}
}
if (System.nanoTime() < stopTime)
return false;
NANOSECONDS.sleep(fixedDelay + rnd.nextLong() % randMod);
}
}
private static final int DELAY_FIXED = 1;
private static final int DELAY_RANDOM = 2;
static long getFixedDelayComponentNanos(long timeout, TimeUnit unit) {
return DELAY_FIXED;
}
static long getRandomDelayModulusNanos(long timeout, TimeUnit unit) {
return DELAY_RANDOM;
}
static class DollarAmount implements Comparable<DollarAmount> {
public int compareTo(DollarAmount other) {
return 0;
}
DollarAmount(int dollars) {
}
}
class Account {
public Lock lock;
void debit(DollarAmount d) {
}
void credit(DollarAmount d) {
}
DollarAmount getBalance() {
return null;
}
}
class InsufficientFundsException extends Exception {
}
}
超时锁—— Lock.tryLock(long timeout, TimeUnit unit):如果该锁定没有被另一个线程保持,并且立即返回 true 值,否则等待直到获取到锁或超时或被中断为止。如果为了使用公平的排序策略,已经设置此锁定,并且其他线程都在等待该锁定,则不会获取一个可用的锁定,这与 tryLock() 方法相反。如果想使用一个允许闯入公平锁定的定时 tryLock,那么可以将定时形式和不定时形式组合在一起:
if (lock.tryLock() || lock.tryLock(timeout, unit) ) { … }
并且如果当前线程,在进入此方法时已经设置了该线程的中断状态,或者在等待获取锁定的同时轮循检测发现中断请求时则抛出 InterruptedException,并且清除当前线程的已中断状态。如果超出了指定的等待时间,则返回值为 false。如果该时间小于或等于 0,则此方法根本不会等待。所以超时锁是一个非常有用的锁,因为它允许程序打破死锁。
在实现具有时间限制地操作时,定时锁同样非常有用。当在带有时间限制的操作中调用了一个阻塞方法时,它能根据剩余时间来提供一个时限。如果操作不能在指定的时间内给出结果,那么就会使程序提前结束。当使用内置锁时,在开始请求锁后,这个操作将无法取消,因此内置锁很难实现带有时间限制的操作。
定时的tryLock能够在这种带有时间限制地操作中实现独占加锁行为。
public class TimedLocking {
private Lock lock = new ReentrantLock();
public boolean trySendOnSharedLine(String message,
long timeout, TimeUnit unit)
throws InterruptedException {
long nanosToLock = unit.toNanos(timeout)
- estimatedNanosToSend(message);
if (!lock.tryLock(nanosToLock, NANOSECONDS))
return false;
try {
return sendOnSharedLine(message);
} finally {
lock.unlock();
}
}
private boolean sendOnSharedLine(String message) {
/* send something */
return true;
}
long estimatedNanosToSend(String message) {
return message.length();
}
}
13.1.2 可中断的锁获取操作
中断锁 —— Lock.lockInterruptibly():该锁与lock相似,但可以被中断。它又相当于一个超时为无限的tryLocky方法。如果线程未被中断,也不能获取到锁,就会一直阻塞下去,直到获取到锁或发生中断请求。如果当前线程在进入此方法时已经设置了该线程的中断状态,或者在等待获取锁定的同时轮循检测发现中断请求时则抛出 InterruptedException,(其他两种不会抛此种异常),并且清除当前线程的已中断状态。
lockInterruptibly方法能够在获得锁的同时保持对中断的的响应,并且由于它包含在Lock中,因此无需创建其他类型的不可中断阻塞机制。
可中断的锁获取操作的标准结构比普通的锁获取操作略微复杂一些,因为需要两个try块。(如果在可中断的锁获取操作中抛出了InterruptedException,那么可以使用标准的try-finally加锁模式。)定时的tryLock同样能响应中断,因为当需要实现一个定时的和可中断的锁获取操作时,可以使用tryLock方法。
public class InterruptibleLocking {
private Lock lock = new ReentrantLock();
public boolean sendOnSharedLine(String message)
throws InterruptedException {
lock.lockInterruptibly();
try {
return cancellableSendOnSharedLine(message);
} finally {
lock.unlock();
}
}
private boolean cancellableSendOnSharedLine(String message) throws InterruptedException {
/* send something */
return true;
}
}
13.1.3 非块结构的加锁
在内置锁中,锁的获取和释放等操作都是基于代码块的——释放锁的操作总是与获取锁的操作处于同一个代码块,而不考虑控制权如果退出该代码块。自动的锁释放操作简化了对程序的分析,避免了可能的编码错误,但有时候需要更灵活的加锁规则。
连锁式加锁或者锁耦合。
13.2 性能考虑因素
当把ReentrantLock添加到Java5.0时,它能比内置锁提供更好的竞争性能。对于同步原语来说,竞争性能是可伸缩性的关键因素:如果有越多的资源被耗费在锁的管理和调度上,那么应用程序得到的资源就越少。锁的实现方式越好,将需要越少的系统调用和上下文切换,并且在共享内存总线上的内存同步通信量也越少,而一些耗时的操作将占用应用程序的计算资源。
Java6使用了改进后的算法来管理内置锁,与在ReentrantLock中使用的算法类似,该算法有效地提高了可伸缩性。图中的曲线表示在某个JVM版本中ReentrantLock相对于内置锁的“加速比”。在Java5.0中,ReentrantLock能提供更高的吞吐量,但在Java6中,二者的吞吐量非常接近。
在Java5.0中,当从单线程(无竞争)变化到多线程时,内置锁的性能急剧下降,而ReentrantLock的性能下降则更加平缓,因而它具有更好的可伸缩性。但在Java6中,情况就完全不同了,内置锁的性能不会由于竞争而几句下降,而且两者的可伸缩性也基本相当。
上面曲线告诉我们,像”X比Y更快“这样的表述大多数是短暂的。性能和可伸缩性对于具体平台等因素都较为敏感,例如CPU,处理器数量、缓存大小以及JVM特性等,所有这些因素都可能会随着时间而发生变化。
性能是一个不断变化的指标,如果在昨天的测试基准中发现X比Y更快,那么在今天就可能已经过时了。
13.3 公平性
公平锁——在你构建一个ReentrantLock时,你可以指定你需要一个公平锁策略:
Lock fairLock = new ReentrantLock(true);
公平锁策略会优待那些等待了最长时间的线程。但是,保证公平性可能会大大影响性能。因此,在默认情况,锁也不需要是公平的。
即便你使用了公平锁,你也不能保证线程调度器是公平的。如果线程调度器选择忽略一个等待了很长时间的线程,那么该线程就没有机会得到锁的公平对待。虽然公平锁听起来不错,但公平锁比普通锁要慢很多。因此,只有理由充分的情况下,才使用公平锁。
在ReentrantLock的构造函数中提供了两种公平性选择:创建一个非公平的锁(默认)或者一个公平的锁。在公平的锁上,线程将按照它们发出请求的顺序来获得锁,但在非公平的锁上,则允许”插队“:当一个线程请求非公平的锁时,如果在发出请求的同时该锁的状态变为可用,那么这个线程将跳过队列中所有的等待线程并获得这个锁。(在Semaphore中同样可以选择采用公平的或非公平的获取顺序。)非公平的ReentrantLock并不提倡”插队“行为,但无法防止某个线程在合适的时候进行”插队“。在公平的锁中,如果有一个线程持有这个锁或者其他线程在队列中等待这个锁,那么新发出请求的线程将被放入队列中。在非公平的锁中,只有当锁被某个线程持有时,新发出请求的线程才会被放入队列中。(即使对于公平锁而言,可轮询的tryLock仍然会”插队“)
我们为什么不希望所有的锁是公平的?当执行加锁行为时,公平性将由于在挂起线程和恢复线程时存在的开销而极大地降低性能。在实际情况中,统计上的公平性保护——确保被阻塞的线程能最终获得锁,通常已经够用了,并且实际开销也小得多。有些算法依赖于公平的排队算法以确保它们的正确性,但这些算法并不常见。在大多数情况下,非公平锁的性能要高于公平锁的性能。
下面给出了Map的性能测试,并比较由公平的以及非公平的ReentrantLock包装的HashMap的性能,从图中可以看出,公平性把性能降低了约两个数量级。不必要的话,不要为公平性付出代价:
在激烈竞争的情况下,非公平锁的性能高于公平锁的性能的一个原因是:在恢复一个被挂起的线程与该线程真正开始运行之间存在严重的延迟。假设线程A持有一个锁,并且线程B请求这个锁。由于这个锁已被线程A持有,因此B将被挂起。当A释放锁时,B将被唤醒,因此会再次尝试获取锁。与此同时,如果C也请求这个锁,那么C很可能会在B被完全唤醒之前获得、使用以及释放这个锁。这样的情况是一种”双赢“的局面:B获得锁的时刻并没有推迟,C更早地获得了锁,并且吞吐量也获得了提高。
当持有锁的时间相对较长,或者请求锁的平局时间间隔较长,那么应该使用公平锁。在这些情况下,“插队”带来的吞吐量提升(当锁处于可用状态时,线程却还未处于被唤醒的过程中)则可能不会出现。
与默认的ReentrantLock一样,内置加锁并不会提供确定的公平性保证,但在大多数情况下,在锁实现上实现统计的公平性保证已经足够了。Java语言规范并没有要求JVM以公平的方式来实现内置锁,而在各种JVM中也没有这样做。ReentrantLock并没有进一步降低锁的公平性,而只是使一些已经存在内容更明显。
13.4 在synchronized和ReentrantLock之间进行选择
ReentrantLock在加锁和内存上提供的语义与内置锁相同,此外它还提供了一些其他功能,包括定时的锁等待、可中断的锁等待、公平锁,以及实现非块结构的加锁。ReentrantLock在性能上似乎优于内置锁,其中在Java6中略有胜出,而在Java5.0中则是远远胜出。
与显式锁相比,内置锁仍然具有很大的优势。内置锁为许多开发人员锁熟悉,并且简洁紧凑,而且在许多现有的程序中都已经使用了内置锁——如果将这两种机制混合使用,那么不仅容易令人困惑,也容易发生错误。ReentrantLock的危险性比同步机制要高,如果忘记在finally块中调用unlock,那么虽然代码表面上能正常运行,但实际上已经埋下了一颗定时炸弹,并很可能伤及其他代码。仅当内置锁不能满足需求时,才可以考虑使用ReentrantLock。
在一些内置锁无法满足需求的情况下,ReentrantLock可以作为一种高级工具。当需要一些高级功能时才应该使用ReentrantLock,这些功能包括:可定时的、可轮询的与可中断的锁获取操作,公平队列,以及非块结构的锁。否则,还是应该优先使用synchronized。
在Java5.0中,内置锁与ReentrantLock相比还有另一个优点:在线程转储中能给出在哪些调用帧中获得了哪些锁,并能够检测和识别发生死锁的线程。JVM并不知道哪些线程持有ReentrantLock,因此在调试使用ReentrantLock的线程的问题时,将起不到帮助作用。Java6解决了这个问题,它提供了一个管理和调试接口,锁可以通过该接口进行注册,从而与ReentrantLocks相关的加锁信息就能出现在转储中,并通过其他的管理接口和调试接口来访问。与synchronized相比,这些调试信息是一种重要的优势,即便它们大部分都是临时性消息,线程转储中的加锁能给很多程序员带来帮助。ReentrantLock的非块结构特性仍然意味着,获取锁的操作不能与特定的栈帧关联起来,而内置锁却可以。
JVM使用线程转储可以帮助你识别死锁的发生。线程转储包括每个运行中线程的栈追踪信息,以及与之相关发生的异常。线程转储也包括锁的信息,比如,哪个锁由哪个线程获得,其中获得这些锁的栈结构,以及阻塞线程正在等待的锁究竟是哪一个(即使你没有遇到死锁,这些信息在调试中也是有用处的;周期性的触发线程转储可以让你观察到程序加锁的行为)。在生成线程转储之前,JVM在“正在等待(is-waiting-for)”关系(有向)图中搜索循环来寻找死锁,如果发现了死锁,它就会包括死锁的识别信息,其中哪些线程参了这个锁,以及死锁发生的位置。
在Unix平台,你可以通过向JVM的进程发送SIGQUIT信号(kill -3)来触发线程转储,或者是在Unix平台下Ctrl - \ 键,在Windows平台下按 Ctrl – Break 键。下面是在某次死锁时线程转储结果:
...
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x00a8710c (object 0x22b0f4b8, a java.lang.Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x00a870ec (object 0x22b0f4c0, a java.lang.Object),
which is held by "Thread-1"
Java stack information for the threads listed above:
===================================================
"Thread-1":
at test.TestDeathLock.deathLock(TestDeathLock.java:9)
- waiting to lock <0x22b0f4b8> (a java.lang.Object)
- locked <0x22b0f4c0> (a java.lang.Object)
at test.TestDeathLock$2.run(TestDeathLock.java:29)
"Thread-0":
at test.TestDeathLock.deathLock(TestDeathLock.java:9)
- waiting to lock <0x22b0f4c0> (a java.lang.Object)
- locked <0x22b0f4b8> (a java.lang.Object)
at test.TestDeathLock$1.run(TestDeathLock.java:22)
Found 1 deadlock.
未来更可能会提升synchronized而不是ReentrantLock的性能。因为synchronized是JVM的内置属性,它能执行一些优化,例如对线程封闭的对象的锁消除优化,通过增加锁的粒度来消除内置锁的同步,而如果通过基于类库的锁来实现这些功能,则可能性不大。除非将来需要在Java5.0上部署应用程序,并且在该平台上确实需要ReentrantLock包含的可伸缩性,否则就性能方面来说,应该选择synchronized而不是ReentrantLock。
13.5 读 - 写锁
ReentrantLock实现了标准的互斥锁(它好比是独占锁):一次最多只有一个线程能够持有相同ReentrantLock。互斥锁避免了“写/写”和“写/读”的重叠,但是同样也避开了“读/读”的重叠,但实质上“读/读”在绝大多数情况下是允许的。
只要每个线程都能确保读取到最新的数据,并且在读取数据时不会有其他的线程修改数据,那么就不会发生问题。在这种情况下就可以使用读 / 写锁:一个资源可以被多个读操作访问,或者被一个写操作访问,但两者不能同时进行。
ReadWriteLock中暴露了两个Lock对象,其中一个用于读操作,而另一个用于写操作。要读取由ReadWriteLock保护的数据,必须首先获得读取锁,当需要修改ReadWriteLock保护的数据时,必须首先获得写入锁。尽管这两个锁看上去是彼此独立的,但读取锁和写入锁只是读 - 写锁对象的不同视图。
public interface ReadWriteLock
{
Lock readLock(); //得到一个可被多个读操作共用的读锁,但它会排斥所有写操作。
Lock writeLock(); //得到一个写锁,它会排斥所有其他的读操作和写操作。
}
读 - 写锁是一种性能优化措施,在一些特定的情况下能实现更高的并发性。在实际情况中,对于在多处理器系统上被频繁读取的数据结构,读 - 写锁能够提高性能。而在其他情况下,读 - 写锁的性能比独占锁的性能要略差一些,这是因为它们的复杂性更高。如果要判断在某种情况下使用读 - 写锁是否会带来性能提升,最好对程序进行分析。由于ReadWriteLock使用Lock来实现锁的读 - 写部分,因此如果分析结果表明读 - 写锁没有提高性能,那么可以很容易地将读 - 写锁换为独占锁。
在读写锁和写入锁之间的交互可以采用多种实现方式。ReadWriteLock中的一些可选实现包括:
- 释放优先。当一个写入操作释放写入锁时,并且队列中同时存在读线程和写线程,那么应该优先选择读线程,写线程,还是最先发出请求的线程?
- 读线程插队。如果锁是由读线程持有,但有写线程正在等待,那么新到达的读线程能否立即获得访问权,还是应该在写线程后面等待?如果允许读线程插队到写线程之前,那么将提高并发性,但却可能造成写线程饥饿问题。
- 重入性。读取锁和写入锁是否是可重入的?
- 降级。如果一个线程持有写入锁,那么它能否在不释放该锁的情况下获得读取锁?这可能会使得写入锁被“降级”为读取锁,同时不允许其他写线程修改被保护的资源。
- 升级。读取锁能否优先于其他正在等待的读线程和写线程而升级为一个写入锁?在大多数的读 - 写锁实现中并不支持升级,因为如果没有显式地升级操作,那么很容易造成死锁。(如果两个读线程试图同时升级为写入锁,那么二者都不会释放读取锁。)
ReentrantReadWriteLock为这两种锁都提供了可重入的加锁语义。与ReentrantLock类似,ReentrantReadWriteLock在构造时也可以选择是一个非公平的锁(默认)还是一个公平的锁。在公平的锁中,等待时间最长的线程将优先获得锁。如果这个锁由读线程持有,而另一个线程请求写入锁,那么其他读线程都不能获得读取锁,直到写线程使用完并且释放了写入锁。在非公平的锁中,线程获得访问许可的顺序是不确定的。写线程降级为读线程是可以的,当从读线程升级为写线程这是不可以的(这样会导致死锁)。
与ReentrantLock类似地是,ReentrantReadWriteLock中的写入锁只有有唯一的所有者,并且只能由获得该锁的线程来释放。
当锁的持有时间较长并且大部分操作都不会修改被守护的资源时,那么读 - 写锁能提高并发性。现实中,ConcurrentHashMap的性能已经足够好了,所以你可以使用它,而不必使用这个新的解决方案,只有你需要的是别的Map时(例如LinkedHashMap),那么这技术是非常有用的。下面使用ReentrantReadWriteLock构建的Map,允许多个get操作并发执行:
public class ReadWriteMap<K,V> {
private Map<K,V> map;
private ReadWriteLock lock = new ReentrantReadWriteLock();
private Lock readLock = lock.readLock();
private Lock writeLock = lock.writeLock();
public ReadWriteMap(Map<K,V> map){
this.map = map;
}
public V get(K key){
readLock.lock();
try{
return map.get(key);
}
finally{
readLock.unlock();
}
}
// 对其他只读的Map方法执行相同的操作
public void put(K key,V value){
writeLock.lock();
try{
map.put(key, value);
}
finally{
writeLock.unlock();
}
}
// 对romove() ,putAll(), clear()等方法执行相同的操作
}
重入还允许从写入锁定降级为读取锁定,其实现方式是:先获取写入锁定,然后获取读取锁定(此时前面的写锁会自动降级为读锁),最后释放写入锁定。但是,从读取锁定升级到写入锁定是不可能的,因为容易引起死锁。
下面的代码展示了如何利用重入来执行升级缓存后的锁定降级(为简单起见,省略了异常处理):
class CachedData {
Object data;
volatile boolean cacheValid;
ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
/*
* 获取读锁,或以允许多个线程进入(读区间)。这里是为了同步访问 cacheValid共
* 享数据,如果已产生写锁(即已有线程进入到写区间后)则阻塞
*/
rwl.readLock().lock();
if (!cacheValid) {// 读数据
/*
* 在获取写锁前一定要先解锁,并且所有已获取该读锁的所有线程都需释放后,
* 才能获取写锁
*/
rwl.readLock().unlock();
// 获取写锁,在这之前读区间上的所有线程需释放所有的读锁
rwl.writeLock().lock();
if (!cacheValid) { // recheck
data = ...//写数据
cacheValid = true;// 写数据
}
// 准备进入读区间,现在写锁开始降级为读锁,所以才能在没有释放写锁时获
// 取读锁,这里不能将下面两行位置交换,因为如果这样则不能保证读原子性了
rwl.readLock().lock(); // 因为写锁降级,在获取读锁前不需要先释放写锁
rwl.writeLock().unlock(); // 释放写锁,但读锁仍然还在
}
use(data);//使用数据
rwl.readLock().unlock();// 使用完后释放读锁
}
}
写锁降级多用在对一片数据操作时,前部分需要写,而后部分只需要读,但读操作以需要与前部分数据保持一致与同步,所以此时就需要用来锁降级,就像上面那样。
小结
与内置锁相比,显式的Lock提供了一些扩展功能,在处理锁的不可用性的方面有着更高的灵活性,并且对队列有着更高的控制。但ReentrantLock不能完全替代synchronized,只有在synchronized无法满足需求时,才应该使用它。
读 - 写锁允许多个读线程并发地访问被保护的对象,当访问以读取操作为主的数据结构时,它能提高程序的可伸缩性。