java并发(十三)嵌套管程锁死
嵌套管程锁死类似于死锁, 下面是一个嵌套管程锁死的场景:
线程1获得对象B的锁(同时持有对象A的锁)。
线程1决定等待另一个线程的信号再继续。
线程1调用B.wait(),从而释放了B对象上的锁,但仍然持有对象A的锁。
线程2需要同时持有对象A和对象B的锁,才能向线程1发信号。
线程2无法获得对象A上的锁,因为对象A上的锁当前正被线程1持有。
线程2一直被阻塞,等待线程1释放对象A上的锁。
线程1一直阻塞,等待线程2的信号,因此,不会释放对象A上的锁,
而线程2需要对象A上的锁才能给线程1发信号……
你可以能会说,这是个空想的场景,好吧,让我们来看看下面这个比较挫的Lock实现:
- //lock implementation with nested monitor lockout problem
- public class Lock{
- protected MonitorObject monitorObject = new MonitorObject();
- protected boolean isLocked = false;
- public void lock() throws InterruptedException{
- synchronized(this){
- while(isLocked){
- synchronized(this.monitorObject){
- this.monitorObject.wait();
- }
- }
- isLocked = true;
- }
- }
- public void unlock(){
- synchronized(this){
- this.isLocked = false;
- synchronized(this.monitorObject){
- this.monitorObject.notify();
- }
- }
- }
- }
可以看到,lock()方法首先在”this”上同步,然后在monitorObject上同步。如果isLocked等于false,因为线程不会继续调用monitorObject.wait(),那么一切都没有问题 。但是如果isLocked等于true,调用lock()方法的线程会在monitorObject.wait()上阻塞。
这里的问题在于,调用monitorObject.wait()方法只释放了monitorObject上的管程对象,而与”this“关联的管程对象并没有释放。换句话说,这个刚被阻塞的线程仍然持有”this”上的锁。
(校对注:如果一个线程持有这种Lock的时候另一个线程执行了lock操作)当一个已经持有这种Lock的线程想调用unlock(),就会在unlock()方法进入synchronized(this)块时阻塞。这会一直阻塞到在lock()方法中等待的线程离开synchronized(this)块。但是,在unlock中isLocked变为false,monitorObject.notify()被执行之后,lock()中等待的线程才会离开synchronized(this)块。
简而言之,在lock方法中等待的线程需要其它线程成功调用unlock方法来退出lock方法,但是,在lock()方法离开外层同步块之前,没有线程能成功执行unlock()。
结果就是,任何调用lock方法或unlock方法的线程都会一直阻塞。这就是嵌套管程锁死。
一个更现实的例子
你可能会说,这么挫的实现方式我怎么可能会做呢?你或许不会在里层的管程对象上调用wait或notify方法,但完全有可能会在外层的this上调。
有很多类似上面例子的情况。例如,如果你准备实现一个公平锁。你可能希望每个线程在它们各自的QueueObject上调用wait(),这样就可以每次唤醒一个线程。
下面是一个比较挫的公平锁实现方式:
- //Fair Lock implementation with nested monitor lockout problem
- public class FairLock {
- private boolean isLocked = false;
- private Thread lockingThread = null;
- private List waitingThreads = new ArrayList();
- public void lock() throws InterruptedException{
- QueueObject queueObject = new QueueObject();
- synchronized(this){
- waitingThreads.add(queueObject);
- while(isLocked || waitingThreads.get(0) != queueObject){
- synchronized(queueObject){
- try{
- queueObject.wait();
- }catch(InterruptedException e){
- waitingThreads.remove(queueObject);
- throw e;
- }
- }
- }
- waitingThreads.remove(queueObject);
- isLocked = true;
- lockingThread = Thread.currentThread();
- }
- }
- public synchronized void unlock(){
- if(this.lockingThread != Thread.currentThread()){
- throw new IllegalMonitorStateException(
- "Calling thread has not locked this lock");
- }
- isLocked = false;
- lockingThread = null;
- if(waitingThreads.size() > 0){
- QueueObject queueObject = waitingThread.get(0);
- synchronized(queueObject){
- queueObject.notify();
- }
- }
- }
- }
- public class QueueObject {}
乍看之下,嗯,很好,但是请注意lock方法是怎么调用queueObject.wait()的,在方法内部有两个synchronized块,一个锁定this,一个嵌在上一个synchronized块内部,它锁定的是局部变量queueObject。
当一个线程调用queueObject.wait()方法的时候,它仅仅释放的是在queueObject对象实例的锁,并没有释放”this”上面的锁。
现在我们还有一个地方需要特别注意, unlock方法被声明成了synchronized,这就相当于一个synchronized(this)块。这就意味着,如果一个线程在lock()中等待,该线程将持有与this关联的管程对象。所有调用unlock()的线程将会一直保持阻塞,等待着前面那个已经获得this锁的线程释放this锁,但这永远也发生不了,因为只有某个线程成功地给lock()中等待的线程发送了信号,this上的锁才会释放,但只有执行unlock()方法才会发送这个信号。
因此,上面的公平锁的实现会导致嵌套管程锁死。更好的公平锁实现方式可以参考Starvation and Fairness。
嵌套管程锁死 VS 死锁
嵌套管程锁死与死锁很像:都是线程最后被一直阻塞着互相等待。
但是两者又不完全相同。在死锁中我们已经对死锁有了个大概的解释,死锁通常是因为两个线程获取锁的顺序不一致造成的,线程1锁住A,等待获取B,线程2已经获取了B,再等待获取A。如死锁避免中所说的,死锁可以通过总是以相同的顺序获取锁来避免。
但是发生嵌套管程锁死时锁获取的顺序是一致的。线程1获得A和B,然后释放B,等待线程2的信号。线程2需要同时获得A和B,才能向线程1发送信号。所以,一个线程在等待唤醒,另一个线程在等待想要的锁被释放。
不同点归纳如下:
嵌套管程锁死中,线程1持有锁A,同时等待从线程2发来的信号,线程2需要锁A来发信号给线程1。
java并发(十四)Slipped Conditions
所谓Slipped conditions,就是说, 从一个线程检查某一特定条件到该线程操作此条件期间,这个条件已经被其它线程改变,导致第一个线程在该条件上执行了错误的操作。这里有一个简单的例子:
- public class Lock {
- private boolean isLocked = true;
- public void lock() {
- synchronized (this) {
- while (isLocked) {
- try {
- this.wait();
- } catch (InterruptedException e) {
- // do nothing, keep waiting
- }
- }
- }
- synchronized (this) {
- isLocked = true;
- }
- }
- public synchronized void unlock() {
- isLocked = false;
- this.notify();
- }
- }
我们可以看到,lock()方法包含了两个同步块。第一个同步块执行wait操作直到isLocked变为false才退出,第二个同步块将isLocked置为true,以此来锁住这个Lock实例避免其它线程通过lock()方法。
我们可以设想一下,假如在某个时刻isLocked为false, 这个时候,有两个线程同时访问lock方法。如果第一个线程先进入第一个同步块,这个时候它会发现isLocked为false,若此时允许第二个线程执行,它也进入第一个同步块,同样发现isLocked是false。现在两个线程都检查了这个条件为false,然后它们都会继续进入第二个同步块中并设置isLocked为true。
这个场景就是slipped conditions的例子,两个线程检查同一个条件, 然后退出同步块,因此在这两个线程改变条件之前,就允许其它线程来检查这个条件。换句话说,条件被某个线程检查到该条件被此线程改变期间,这个条件已经被其它线程改变过了。
为避免slipped conditions,条件的检查与设置必须是原子的,也就是说,在第一个线程检查和设置条件期间,不会有其它线程检查这个条件。
解决上面问题的方法很简单,只是简单的把isLocked = true这行代码移到第一个同步块中,放在while循环后面即可:
- public class Lock {
- private boolean isLocked = true;
- public void lock() {
- synchronized (this) {
- while (isLocked) {
- try {
- this.wait();
- } catch (InterruptedException e) {
- // do nothing, keep waiting
- }
- }
- isLocked = true;
- }
- }
- public synchronized void unlock() {
- isLocked = false;
- this.notify();
- }
- }
现在检查和设置isLocked条件是在同一个同步块中原子地执行了。
一个更现实的例子
也许你会说,我才不可能写这么挫的代码,还觉得slipped conditions是个相当理论的问题。但是第一个简单的例子只是用来更好的展示slipped conditions。
饥饿和公平中实现的公平锁也许是个更现实的例子。再看下嵌套管程锁死中那个幼稚的实现,如果我们试图解决其中的嵌套管程锁死问题,很容易产生slipped conditions问题。 首先让我们看下嵌套管程锁死中的例子:
- //Fair Lock implementation with nested monitor lockout problem
- public class FairLock {
- private boolean isLocked = false;
- private Thread lockingThread = null;
- private List waitingThreads = new ArrayList();
- public void lock() throws InterruptedException{
- QueueObject queueObject = new QueueObject();
- synchronized(this){
- waitingThreads.add(queueObject);
- while(isLocked || waitingThreads.get(0) != queueObject){
- synchronized(queueObject){
- try{
- queueObject.wait();
- }catch(InterruptedException e){
- waitingThreads.remove(queueObject);
- throw e;
- }
- }
- }
- waitingThreads.remove(queueObject);
- isLocked = true;
- lockingThread = Thread.currentThread();
- }
- }
- public synchronized void unlock(){
- if(this.lockingThread != Thread.currentThread()){
- throw new IllegalMonitorStateException("Calling thread has not locked this lock");
- }
- isLocked = false;
- lockingThread = null;
- if(waitingThreads.size() > 0){
- QueueObject queueObject = waitingThread.get(0);
- synchronized(queueObject){
- queueObject.notify();
- }
- }
- }
- }
- public class QueueObject {}
我们可以看到synchronized(queueObject)及其中的queueObject.wait()调用是嵌在synchronized(this)块里面的,这会导致嵌套管程锁死问题。为避免这个问题,我们必须将synchronized(queueObject)块移出synchronized(this)块。移出来之后的代码可能是这样的:
- //Fair Lock implementation with slipped conditions problem
- public class FairLock {
- private boolean isLocked = false;
- private Thread lockingThread = null;
- private List waitingThreads = new ArrayList();
- public void lock() throws InterruptedException{
- QueueObject queueObject = new QueueObject();
- synchronized(this){
- waitingThreads.add(queueObject);
- }
- boolean mustWait = true;
- while(mustWait){
- synchronized(this){
- mustWait = isLocked || waitingThreads.get(0) != queueObject;
- }
- synchronized(queueObject){
- if(mustWait){
- try{
- queueObject.wait();
- }catch(InterruptedException e){
- waitingThreads.remove(queueObject);
- throw e;
- }
- }
- }
- }
- synchronized(this){
- waitingThreads.remove(queueObject);
- isLocked = true;
- lockingThread = Thread.currentThread();
- }
- }
- }
注意:因为我只改动了lock()方法,这里只展现了lock方法。
现在lock()方法包含了3个同步块。
第一个,synchronized(this)块通过mustWait = isLocked || waitingThreads.get(0) != queueObject检查内部变量的值。
第二个,synchronized(queueObject)块检查线程是否需要等待。也有可能其它线程在这个时候已经解锁了,但我们暂时不考虑这个问题。我们就假设这个锁处在解锁状态,所以线程会立马退出synchronized(queueObject)块。
第三个,synchronized(this)块只会在mustWait为false的时候执行。它将isLocked重新设回true,然后离开lock()方法。
设想一下,在锁处于解锁状态时,如果有两个线程同时调用lock()方法会发生什么。首先,线程1会检查到isLocked为false,然后线程2同样检查到isLocked为false。接着,它们都不会等待,都会去设置isLocked为true。这就是slipped conditions的一个最好的例子。
解决Slipped Conditions问题
要解决上面例子中的slipped conditions问题,最后一个synchronized(this)块中的代码必须向上移到第一个同步块中。为适应这种变动,代码需要做点小改动。下面是改动过的代码:
- //Fair Lock implementation without nested monitor lockout problem,
- //but with missed signals problem.
- public class FairLock {
- private boolean isLocked = false;
- private Thread lockingThread = null;
- private List waitingThreads = new ArrayList();
- public void lock() throws InterruptedException{
- QueueObject queueObject = new QueueObject();
- synchronized(this){
- waitingThreads.add(queueObject);
- }
- boolean mustWait = true;
- while(mustWait){
- synchronized(this){
- mustWait = isLocked || waitingThreads.get(0) != queueObject;
- if(!mustWait){
- waitingThreads.remove(queueObject);
- isLocked = true;
- lockingThread = Thread.currentThread();
- return;
- }
- }
- synchronized(queueObject){
- if(mustWait){
- try{
- queueObject.wait();
- }catch(InterruptedException e){
- waitingThreads.remove(queueObject);
- throw e;
- }
- }
- }
- }
- }
- }
我们可以看到对局部变量mustWait的检查与赋值是在同一个同步块中完成的。还可以看到,即使在synchronized(this)块外面检查了mustWait,在while(mustWait)子句中,mustWait变量从来没有在synchronized(this)同步块外被赋值。当一个线程检查到mustWait是false的时候,它将自动设置内部的条件(isLocked),所以其它线程再来检查这个条件的时候,它们就会发现这个条件的值现在为true了。
synchronized(this)块中的return;语句不是必须的。这只是个小小的优化。如果一个线程肯定不会等待(即mustWait为false),那么就没必要让它进入到synchronized(queueObject)同步块中和执行if(mustWait)子句了。
细心的读者可能会注意到上面的公平锁实现仍然有可能丢失信号。设想一下,当该FairLock实例处于锁定状态时,有个线程来调用lock()方法。执行完第一个 synchronized(this)块后,mustWait变量的值为true。再设想一下调用lock()的线程是通过抢占式的,拥有锁的那个线程那个线程此时调用了unlock()方法,但是看下之前的unlock()的实现你会发现,它调用了queueObject.notify()。但是,因为lock()中的线程还没有来得及调用queueObject.wait(),所以queueObject.notify()调用也就没有作用了,信号就丢失掉了。如果调用lock()的线程在另一个线程调用queueObject.notify()之后调用queueObject.wait(),这个线程会一直阻塞到其它线程调用unlock方法为止,但这永远也不会发生。
公平锁实现的信号丢失问题在饥饿和公平一文中我们已有过讨论,把QueueObject转变成一个信号量,并提供两个方法:doWait()和doNotify()。这些方法会在QueueObject内部对信号进行存储和响应。用这种方式,即使doNotify()在doWait()之前调用,信号也不会丢失。
自Java 5开始,java.util.concurrent.locks包中包含了一些锁的实现,因此你不用去实现自己的锁了。但是你仍然需要去了解怎样使用这些锁,且了解这些实现背后的理论也是很有用处的。
以下是本文所涵盖的主题:
一个简单的锁
锁的可重入性
锁的公平性
在finally语句中调用unlock()
一个简单的锁
让我们从java中的一个同步块开始:
- public class Counter{
- private int count = 0;
- public int inc(){
- synchronized(this){
- return ++count;
- }
- }
- }
可以看到在inc()方法中有一个synchronized(this)代码块。该代码块可以保证在同一时间只有一个线程可以执行return ++count。虽然在synchronized的同步块中的代码可以更加复杂,但是++count这种简单的操作已经足以表达出线程同步的意思。
以下的Counter类用Lock代替synchronized达到了同样的目的:
- public class Counter{
- private Lock lock = new Lock();
- private int count = 0;
- public int inc(){
- lock.lock();
- int newCount = ++count;
- lock.unlock();
- return newCount;
- }
- }
lock()方法会对Lock实例对象进行加锁,因此所有对该对象调用lock()方法的线程都会被阻塞,直到该Lock对象的unlock()方法被调用。
这里有一个Lock类的简单实现:
- public class Lock {
- private boolean isLocked = false;
- public synchronized void lock() throws InterruptedException {
- while (isLocked) {
- wait();
- }
- isLocked = true;
- }
- public synchronized void unlock() {
- isLocked = false;
- notify();
- }
- }
注意其中的while(isLocked)循环,它又被叫做“自旋锁”。自旋锁以及wait()和notify()方法在线程通信这篇文章中有更加详细的介绍。当isLocked为true时,调用lock()的线程在wait()调用上阻塞等待。为防止该线程没有收到notify()调用也从wait()中返回(也称作虚假唤醒),这个线程会重新去检查isLocked条件以决定当前是否可以安全地继续执行还是需要重新保持等待,而不是认为线程被唤醒了就可以安全地继续执行了。如果isLocked为false,当前线程会退出while(isLocked)循环,并将isLocked设回true,让其它正在调用lock()方法的线程能够在Lock实例上加锁。
当线程完成了临界区(位于lock()和unlock()之间)中的代码,就会调用unlock()。执行unlock()会重新将isLocked设置为false,并且通知(唤醒)其中一个(若有的话)在lock()方法中调用了wait()函数而处于等待状态的线程。
锁的可重入性
Java中的synchronized同步块是可重入的。这意味着如果一个java线程进入了代码中的synchronized同步块,并因此获得了该同步块使用的同步对象对应的管程上的锁,那么这个线程可以进入由同一个管程对象所同步的另一个java代码块。下面是一个例子:
- public class Reentrant{
- public synchronized void outer(){
- inner();
- }
- public synchronized void inner(){
- //do something
- }
- }
注意outer()和inner()都被声明为synchronized,这在Java中和synchronized(this)块等效。如果一个线程调用了outer(),在outer()里调用inner()就没有什么问题,因为这两个方法(代码块)都由同一个管程对象(”this”)所同步。如果一个线程已经拥有了一个管程对象上的锁,那么它就有权访问被这个管程对象同步的所有代码块。这就是可重入。线程可以进入任何一个它已经拥有的锁所同步着的代码块。
前面给出的锁实现不是可重入的。如果我们像下面这样重写Reentrant类,当线程调用outer()时,会在inner()方法的lock.lock()处阻塞住。
- public class Reentrant2{
- Lock lock = new Lock();
- public void outer(){
- lock.lock();
- inner();
- lock.unlock();
- }
- public void inner(){
- lock.lock();
- //do something
- lock.unlock();
- }
- }
调用outer()的线程首先会锁住Lock实例,然后继续调用inner()。inner()方法中该线程将再一次尝试锁住Lock实例,结果该动作会失败(也就是说该线程会被阻塞),因为这个Lock实例已经在outer()方法中被锁住了。
两次lock()之间没有调用unlock(),第二次调用lock就会阻塞,看过lock()实现后,会发现原因很明显:
- public class Lock {
- private boolean isLocked = false;
- public synchronized void lock() throws InterruptedException {
- while (isLocked) {
- wait();
- }
- isLocked = true;
- }
- public synchronized void unlock() {
- isLocked = false;
- notify();
- }
- }
一个线程是否被允许退出lock()方法是由while循环(自旋锁)中的条件决定的。当前的判断条件是只有当isLocked为false时lock操作才被允许,而没有考虑是哪个线程锁住了它。
为了让这个Lock类具有可重入性,我们需要对它做一点小的改动:
- package com.chinaso.search.executor;
- public class Lock {
- boolean isLocked = false;
- Thread lockedBy = null;
- int lockedCount = 0;
- public synchronized void lock() throws InterruptedException {
- Thread callingThread = Thread.currentThread();
- while (isLocked && lockedBy != callingThread) {
- wait();
- }
- isLocked = true;
- lockedCount++;
- lockedBy = callingThread;
- }
- public synchronized void unlock() {
- if (Thread.currentThread() == this.lockedBy) {
- lockedCount--;
- if (lockedCount == 0) {
- isLocked = false;
- notify();
- }
- }
- }
- }
注意到现在的while循环(自旋锁)也考虑到了已锁住该Lock实例的线程。如果当前的锁对象没有被加锁(isLocked = false),或者当前调用线程已经对该Lock实例加了锁,那么while循环就不会被执行,调用lock()的线程就可以退出该方法(译者注:“被允许退出该方法”在当前语义下就是指不会调用wait()而导致阻塞)。
除此之外,我们需要记录同一个线程重复对一个锁对象加锁的次数。否则,一次unblock()调用就会解除整个锁,即使当前锁已经被加锁过多次。在unlock()调用没有达到对应lock()调用的次数之前,我们不希望锁被解除。
现在这个Lock类就是可重入的了。
锁的公平性
Java的synchronized块并不保证尝试进入它们的线程的顺序。因此,如果多个线程不断竞争访问相同的synchronized同步块,就存在一种风险,其中一个或多个线程永远也得不到访问权 —— 也就是说访问权总是分配给了其它线程。这种情况被称作线程饥饿。为了避免这种问题,锁需要实现公平性。本文所展现的锁在内部是用synchronized同步块实现的,因此它们也不保证公平性。饥饿和公平中有更多关于该内容的讨论。
在finally语句中调用unlock()
如果用Lock来保护临界区,并且临界区有可能会抛出异常,那么在finally语句中调用unlock()就显得非常重要了。这样可以保证这个锁对象可以被解锁以便其它线程能继续对其加锁。以下是一个示例:
- lock.lock();
- try{
- //do critical section code,
- //which may throw exception
- } finally {
- lock.unlock();
- }
这个简单的结构可以保证当临界区抛出异常时Lock对象可以被解锁。如果不是在finally语句中调用的unlock(),当临界区抛出异常时,Lock对象将永远停留在被锁住的状态,这会导致其它所有在该Lock对象上调用lock()的线程一直阻塞。
Java5在java.util.concurrent包中已经包含了读写锁。尽管如此,我们还是应该了解其实现背后的原理。
以下是本文的主题
读/写锁的Java实现(Read / Write Lock Java Implementation)
读/写锁的重入(Read / Write Lock Reentrance)
读锁重入(Read Reentrance)
写锁重入(Write Reentrance)
读锁升级到写锁(Read to Write Reentrance)
写锁降级到读锁(Write to Read Reentrance)
可重入的ReadWriteLock的完整实现(Fully Reentrant ReadWriteLock)
在finally中调用unlock() (Calling unlock() from a finally-clause)
读/写锁的Java实现
先让我们对读写访问资源的条件做个概述:
读取:没有线程正在做写操作,且没有线程在请求写操作。
写入:没有线程正在做读写操作。
如果某个线程想要读取资源,只要没有线程正在对该资源进行写操作且没有线程请求对该资源的写操作即可。我们假设对写操作的请求比对读操作的请求更重要,就要提升写请求的优先级。此外,如果读操作发生的比较频繁,我们又没有提升写操作的优先级,那么就会产生“饥饿”现象。请求写操作的线程会一直阻塞,直到所有的读线程都从ReadWriteLock上解锁了。如果一直保证新线程的读操作权限,那么等待写操作的线程就会一直阻塞下去,结果就是发生“饥饿”。因此,只有当没有线程正在锁住ReadWriteLock进行写操作,且没有线程请求该锁准备执行写操作时,才能保证读操作继续。
当其它线程没有对共享资源进行读操作或者写操作时,某个线程就有可能获得该共享资源的写锁,进而对共享资源进行写操作。有多少线程请求了写锁以及以何种顺序请求写锁并不重要,除非你想保证写锁请求的公平性。
按照上面的叙述,简单的实现出一个读/写锁,代码如下
- public class ReadWriteLock {
- private int readers = 0; //读线程数量
- private int writers = 0; //写线程数量
- private int writeRequests = 0; //写请求线程数量
- /**
- * 读锁
- * @throws InterruptedException
- */
- public synchronized void lockRead() throws InterruptedException {
- while (writers > 0 || writeRequests > 0) {
- wait();
- }
- readers++;
- }
- public synchronized void unlockRead() {
- //synchronized(this)
- readers--;
- notifyAll();
- }
- public synchronized void lockWrite() throws InterruptedException {
- writeRequests++;
- while (readers > 0 || writers > 0) {
- wait();
- }
- writeRequests--;
- writers++;
- }
- public synchronized void unlockWrite() throws InterruptedException {
- writers--;
- notifyAll();
- }
- }
ReadWriteLock类中,读锁和写锁各有一个获取锁和释放锁的方法。
读锁的实现在lockRead()中,只要没有线程拥有写锁(writers==0),且没有线程在请求写锁(writeRequests ==0),所有想获得读锁的线程都能成功获取。
写锁的实现在lockWrite()中,当一个线程想获得写锁的时候,首先会把写锁请求数加1(writeRequests++),然后再去判断是否能够真能获得写锁,当没有线程持有读锁(readers==0 ),且没有线程持有写锁(writers==0)时就能获得写锁。有多少线程在请求写锁并无关系。
需要注意的是,在两个释放锁的方法(unlockRead,unlockWrite)中,都调用了notifyAll方法,而不是notify。要解释这个原因,我们可以想象下面一种情形:
如果有线程在等待获取读锁,同时又有线程在等待获取写锁。如果这时其中一个等待读锁的线程被notify方法唤醒,但因为此时仍有请求写锁的线程存在(writeRequests>0),所以被唤醒的线程会再次进入阻塞状态。然而,等待写锁的线程一个也没被唤醒,就像什么也没发生过一样(译者注:信号丢失现象)。如果用的是notifyAll方法,所有的线程都会被唤醒,然后判断能否获得其请求的锁。
用notifyAll还有一个好处。如果有多个读线程在等待读锁且没有线程在等待写锁时,调用unlockWrite()后,所有等待读锁的线程都能立马成功获取读锁 —— 而不是一次只允许一个。
读/写锁的重入
上面实现的读/写锁(ReadWriteLock) 是不可重入的,当一个已经持有写锁的线程再次请求写锁时,就会被阻塞。原因是已经有一个写线程了——就是它自己。此外,考虑下面的例子:
Thread 1 获得了读锁
Thread 2 请求写锁,但因为Thread 1 持有了读锁,所以写锁请求被阻塞。
Thread 1 再想请求一次读锁,但因为Thread 2处于请求写锁的状态,所以想再次获取读锁也会被阻塞。
上面这种情形使用前面的ReadWriteLock就会被锁定——一种类似于死锁的情形。不会再有线程能够成功获取读锁或写锁了。
为了让ReadWriteLock可重入,需要对它做一些改进。下面会分别处理读锁的重入和写锁的重入。
读锁重入
为了让ReadWriteLock的读锁可重入,我们要先为读锁重入建立规则:
要保证某个线程中的读锁可重入,要么满足获取读锁的条件(没有写或写请求),要么已经持有读锁(不管是否有写请求)。
要确定一个线程是否已经持有读锁,可以用一个map来存储已经持有读锁的线程以及对应线程获取读锁的次数,当需要判断某个线程能否获得读锁时,就利用map中存储的数据进行判断。下面是方法lockRead和unlockRead修改后的的代码:
- import java.util.HashMap;
- import java.util.Map;
- /**
- * 代码中我们可以看到,只有在没有线程拥有写锁的情况下才允许读锁的重入。此外,重入的读锁比写锁优先级高。
- * @author piaohailin
- *
- */
- public class ReadWriteLock {
- private Map<Thread, Integer> readingThreads = new HashMap<Thread, Integer>();
- private int writers = 0;
- private int writeRequests = 0;
- public synchronized void lockRead() throws InterruptedException {
- Thread callingThread = Thread.currentThread();
- while (!canGrantReadAccess(callingThread)) {
- wait();
- }
- readingThreads.put(callingThread, (getReadAccessCount(callingThread) + 1));
- }
- public synchronized void unlockRead() {
- Thread callingThread = Thread.currentThread();
- int accessCount = getReadAccessCount(callingThread);
- if (accessCount == 1) {
- readingThreads.remove(callingThread);
- } else {
- readingThreads.put(callingThread, (accessCount - 1));
- }
- notifyAll();
- }
- /**
- * 判断某一线程是否具有读取权限
- * @param callingThread
- * @return
- */
- private boolean canGrantReadAccess(Thread callingThread) {
- if (writers > 0)// 要么满足获取读锁的条件(没有写或写请求)
- return false;
- if (isReader(callingThread))// 要么已经持有读锁(不管是否有写请求)
- return true;
- if (writeRequests > 0)
- return false;
- return true;
- }
- /**
- * 取得某一读取线程的持有读锁数量
- * @param callingThread
- * @return
- */
- private int getReadAccessCount(Thread callingThread) {
- Integer accessCount = readingThreads.get(callingThread);
- if (accessCount == null)
- return 0;
- return accessCount.intValue();
- }
- /**
- * 判断某一线程是否是读取线程
- * @param callingThread
- * @return
- */
- private boolean isReader(Thread callingThread) {
- return readingThreads.get(callingThread) != null;
- }
- }
代码中我们可以看到,只有在没有线程拥有写锁的情况下才允许读锁的重入。此外,重入的读锁比写锁优先级高。
写锁重入
仅当一个线程已经持有写锁,才允许写锁重入(再次获得写锁)。下面是方法lockWrite和unlockWrite修改后的的代码。
- public class ReadWriteLock{
- private Map<Thread, Integer> readingThreads = new HashMap<Thread, Integer>();
- private int writeAccesses = 0;
- private int writeRequests = 0;
- private Thread writingThread = null;
- public synchronized void lockWrite()
- throws InterruptedException{
- writeRequests++;
- Thread callingThread = Thread.currentThread();
- while(!canGrantWriteAccess(callingThread)){
- wait();
- }
- writeRequests--;
- writeAccesses++;
- writingThread = callingThread;
- }
- public synchronized void unlockWrite()
- throws InterruptedException{
- writeAccesses--;
- if(writeAccesses == 0){
- writingThread = null;
- }
- notifyAll();
- }
- private boolean canGrantWriteAccess(Thread callingThread){
- if(hasReaders()) return false;
- if(writingThread == null) return true;
- if(!isWriter(callingThread)) return false;
- return true;
- }
- private boolean hasReaders(){
- return readingThreads.size() > 0;
- }
- private boolean isWriter(Thread callingThread){
- return writingThread == callingThread;
- }
- }
注意在确定当前线程是否能够获取写锁的时候,是如何处理的。
读锁升级到写锁
有时,我们希望一个拥有读锁的线程,也能获得写锁。想要允许这样的操作,要求这个线程是唯一一个拥有读锁的线程。writeLock()需要做点改动来达到这个目的:
- import java.util.HashMap;
- import java.util.Map;
- /**
- * 现在ReadWriteLock类就可以从读锁升级到写锁了。
- * @author piaohailin
- *
- */
- public class ReadWriteLock {
- private Map<Thread, Integer> readingThreads = new HashMap<Thread, Integer>();
- private int writeAccesses = 0;
- private int writeRequests = 0;
- private int readers = 0;
- private Thread writingThread = null;
- public synchronized void lockWrite() throws InterruptedException {
- writeRequests++;
- Thread callingThread = Thread.currentThread();
- while (!canGrantWriteAccess(callingThread)) {
- wait();
- }
- writeRequests--;
- writeAccesses++;
- writingThread = callingThread;
- }
- public synchronized void unlockWrite() throws InterruptedException {
- writeAccesses--;
- if (writeAccesses == 0) {
- writingThread = null;
- }
- notifyAll();
- }
- private boolean canGrantWriteAccess(Thread callingThread) {
- if (isOnlyReader(callingThread)) //要求这个线程是唯一一个拥有读锁的线程
- return true;
- if (hasReaders())
- return false;
- if (writingThread == null)
- return true;
- if (!isWriter(callingThread))
- return false;
- return true;
- }
- private boolean hasReaders() {
- return readingThreads.size() > 0;
- }
- private boolean isWriter(Thread callingThread) {
- return writingThread == callingThread;
- }
- private boolean isOnlyReader(Thread callingThread) {
- //如果读取线程数为1,且该读取线程持有读取锁
- return readers == 1 && readingThreads.get(callingThread) != null;
- }
- }
现在ReadWriteLock类就可以从读锁升级到写锁了。
写锁降级到读锁
有时拥有写锁的线程也希望得到读锁。如果一个线程拥有了写锁,那么自然其它线程是不可能拥有读锁或写锁了。所以对于一个拥有写锁的线程,再获得读锁,是不会有什么危险的。我们仅仅需要对上面canGrantReadAccess方法进行简单地修改:
- public class ReadWriteLock{
- private boolean canGrantReadAccess(Thread callingThread){
- if(isWriter(callingThread)) return true;
- if(writingThread != null) return false;
- if(isReader(callingThread) return true;
- if(writeRequests > 0) return false;
- return true;
- }
- }
可重入的ReadWriteLock的完整实现
下面是完整的ReadWriteLock实现。为了便于代码的阅读与理解,简单对上面的代码做了重构。重构后的代码如下。
- import java.util.HashMap;
- import java.util.Map;
- public class ReadWriteLock {
- private Map<Thread, Integer> readingThreads = new HashMap<Thread, Integer>();
- private int writeAccesses = 0;
- private int writeRequests = 0;
- private Thread writingThread = null;
- public synchronized void lockRead() throws InterruptedException {
- Thread callingThread = Thread.currentThread();
- while (!canGrantReadAccess(callingThread)) {
- wait();
- }
- readingThreads.put(callingThread, (getReadAccessCount(callingThread) + 1));
- }
- private boolean canGrantReadAccess(Thread callingThread) {
- if (isWriter(callingThread))
- return true;
- if (hasWriter())
- return false;
- if (isReader(callingThread))
- return true;
- if (hasWriteRequests())
- return false;
- return true;
- }
- public synchronized void unlockRead() {
- Thread callingThread = Thread.currentThread();
- if (!isReader(callingThread)) {
- throw new IllegalMonitorStateException("Calling Thread does not" + " hold a read lock on this ReadWriteLock");
- }
- int accessCount = getReadAccessCount(callingThread);
- if (accessCount == 1) {
- readingThreads.remove(callingThread);
- } else {
- readingThreads.put(callingThread, (accessCount - 1));
- }
- notifyAll();
- }
- public synchronized void lockWrite() throws InterruptedException {
- writeRequests++;
- Thread callingThread = Thread.currentThread();
- while (!canGrantWriteAccess(callingThread)) {
- wait();
- }
- writeRequests--;
- writeAccesses++;
- writingThread = callingThread;
- }
- public synchronized void unlockWrite() throws InterruptedException {
- if (!isWriter(Thread.currentThread())) {
- throw new IllegalMonitorStateException("Calling Thread does not" + " hold the write lock on this ReadWriteLock");
- }
- writeAccesses--;
- if (writeAccesses == 0) {
- writingThread = null;
- }
- notifyAll();
- }
- private boolean canGrantWriteAccess(Thread callingThread) {
- if (isOnlyReader(callingThread))
- return true;
- if (hasReaders())
- return false;
- if (writingThread == null)
- return true;
- if (!isWriter(callingThread))
- return false;
- return true;
- }
- private int getReadAccessCount(Thread callingThread) {
- Integer accessCount = readingThreads.get(callingThread);
- if (accessCount == null)
- return 0;
- return accessCount.intValue();
- }
- private boolean hasReaders() {
- return readingThreads.size() > 0;
- }
- private boolean isReader(Thread callingThread) {
- return readingThreads.get(callingThread) != null;
- }
- private boolean isOnlyReader(Thread callingThread) {
- return readingThreads.size() == 1 && readingThreads.get(callingThread) != null;
- }
- private boolean hasWriter() {
- return writingThread != null;
- }
- private boolean isWriter(Thread callingThread) {
- return writingThread == callingThread;
- }
- private boolean hasWriteRequests() {
- return this.writeRequests > 0;
- }
- }
在finally中调用unlock()
在利用ReadWriteLock来保护临界区时,如果临界区可能抛出异常,在finally块中调用readUnlock()和writeUnlock()就显得很重要了。这样做是为了保证ReadWriteLock能被成功解锁,然后其它线程可以请求到该锁。这里有个例子:
- lock.lockWrite();
- try{
- //do critical section code, which may throw exception
- } finally {
- lock.unlockWrite();
- }
上面这样的代码结构能够保证临界区中抛出异常时ReadWriteLock也会被释放。如果unlockWrite方法不是在finally块中调用的,当临界区抛出了异常时,ReadWriteLock 会一直保持在写锁定状态,就会导致所有调用lockRead()或lockWrite()的线程一直阻塞。唯一能够重新解锁ReadWriteLock的因素可能就是ReadWriteLock是可重入的,当抛出异常时,这个线程后续还可以成功获取这把锁,然后执行临界区以及再次调用unlockWrite(),这就会再次释放ReadWriteLock。但是如果该线程后续不再获取这把锁了呢?所以,在finally中调用unlockWrite对写出健壮代码是很重要的。
java的读写锁API
1)java.util.concurrent.locks.ReentrantReadWriteLock
本文中读写锁的标准实现。
示例代码
- import java.util.concurrent.locks.ReentrantReadWriteLock;
- class CachedData {
- Object data;
- volatile boolean cacheValid;
- final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
- void processCachedData() {
- rwl.readLock().lock();
- if (!cacheValid) {
- // Must release read lock before acquiring write lock
- rwl.readLock().unlock();
- rwl.writeLock().lock();
- try {
- // Recheck state because another thread might have
- // acquired write lock and changed state before we did.
- if (!cacheValid) {
- data = ...
- cacheValid = true;
- }
- // Downgrade by acquiring read lock before releasing write lock
- rwl.readLock().lock();
- } finally {
- rwl.writeLock().unlock(); // Unlock write, still hold read
- }
- }
- try {
- use(data);
- } finally {
- rwl.readLock().unlock();
- }
- }
- }
2)java.util.concurrent.locks.StampedLock
在Java 8引入了一个新的读写锁,叫做StampedLock。它不仅更快,同时还提供了一系列强大的API来实现乐观锁,这样如果没有写操作在访问临界区域的话,你只需很低的开销就能获取到一个读锁。访问结束后你可以查询锁来判断这期间是否发生了写操作,如果有的话再选择进行重试,升级锁,或者放弃这个操作。
示例代码
- import java.util.concurrent.locks.StampedLock;
- class Point {
- private double x, y;
- private final StampedLock sl = new StampedLock();
- void move(double deltaX,
- double deltaY) { // an exclusively locked method
- long stamp = sl.writeLock();
- try {
- x += deltaX;
- y += deltaY;
- } finally {
- sl.unlockWrite(stamp);
- }
- }
- double distanceFromOrigin() { // A read-only method
- long stamp = sl.tryOptimisticRead();
- double currentX = x, currentY = y;
- if (!sl.validate(stamp)) {
- stamp = sl.readLock();
- try {
- currentX = x;
- currentY = y;
- } finally {
- sl.unlockRead(stamp);
- }
- }
- return Math.sqrt(currentX * currentX + currentY * currentY);
- }
- void moveIfAtOrigin(double newX,
- double newY) { // upgrade
- // Could instead start with optimistic, not read mode
- long stamp = sl.readLock();
- try {
- while (x == 0.0 && y == 0.0) {
- long ws = sl.tryConvertToWriteLock(stamp);
- if (ws != 0L) {
- stamp = ws;
- x = newX;
- y = newY;
- break;
- } else {
- sl.unlockRead(stamp);
- stamp = sl.writeLock();
- }
- }
- } finally {
- sl.unlock(stamp);
- }
- }
- }