(2.1.27.11)Java并发编程:Lock之ReentrantReadWriteLock 读写分离独享式重入锁

版权声明:本文为博主原创文章,转载请注明出处 https://blog.csdn.net/fei20121106/article/details/83268588

我们在介绍AbstractQueuedSynchronizer的时候介绍过,AQS支持独占式同步状态获取/释放、共享式同步状态获取/释放两种模式,对应的典型应用分别是ReentrantLock和Semaphore

AQS还可以混合两种模式使用,读写锁ReentrantReadWriteLock就是如此。

设想以下情景:我们在系统中有一个多线程访问的缓存,多个线程都可以对缓存进行读或写操作,但是读操作远远多于写操作,要求写操作要线程安全,且写操作执行完成要求对当前的所有读操作马上可见。

分析上面的需求:

  • 因为有多个线程可能会执行写操作,因此多个线程的写操作必须同步串行执行;
  • 而写操作执行完成要求对当前的所有读操作马上可见,这就意味着当有线程正在读的时候,要阻塞写操作,当正在执行写操作时,要阻塞读操作。

一个简单的实现就是将数据直接加上互斥锁,同一时刻不管是读还是写线程,都只能有一个线程操作数据。但是这样的问题就是如果当前只有N个读线程,没有写线程,这N个读线程也要傻呵呵的排队读,尽管其实是可以安全并发提高效率的。

因此理想的实现是:

  1. 当有写线程时,则写线程独占同步状态。
  2. 当没有写线程时只有读线程时,则多个读线程可以共享同步状态。

读写锁就是为了实现这种效果而生。

一、使用示例

我们先来看一下读写锁怎么使用,这里我们基于hashmap(本身线程不安全)做一个多线程并发安全的缓存:

public class ReadWriteCache {
    private static Map<String, Object> data = new HashMap<>();
    private static ReadWriteLock lock = new ReentrantReadWriteLock(false);
    private static Lock rlock = lock.readLock();
    private static Lock wlock = lock.writeLock();

    public static Object get(String key) {
        rlock.lock();
        try {
            return data.get(key);
        } finally {
            rlock.unlock();
        }
    }

    public static Object put(String key, Object value) {
        wlock.lock();
        try {
            return data.put(key, value);
        } finally {
            wlock.unlock();
        }
    }

}

限于篇幅我们只实现2个方法,get和put。从代码可以看出:

  1. 我们先创建一个 ReentrantReadWriteLock 对象,构造函数 false 代表是非公平的(非公平的含义和ReentrantLock相同)。
  2. 然后通过readLock、writeLock方法分别获取读锁和写锁。
    • 在做读操作的时候,也就是get方法,我们要先获取读锁;
    • 在做写操作的时候,即put方法,我们要先获取写锁。

通过以上代码,我们就构造了一个线程安全的缓存,达到我们之前说的:写线程独占同步状态,多个读线程可以共享同步状态。

二、源码分析

2.1 ReentrantReadWriteLock整体结构

public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {

    private final ReentrantReadWriteLock.ReadLock readerLock;
    private final ReentrantReadWriteLock.WriteLock writerLock;
	
    final Sync sync;

    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }

    public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
    public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }


    abstract static class Sync extends AbstractQueuedSynchronizer {}

    static final class NonfairSync extends Sync {}

    static final class FairSync extends Sync {}

    public static class ReadLock implements Lock, java.io.Serializable {}

    public static class WriteLock implements Lock, java.io.Serializable {}
}

可以看到,在公平锁与非公平锁的实现上,与ReentrantLock一样,也是有一个继承AQS的内部类Sync,然后NonfairSync和FairSync都继承Sync,通过构造函数传入的布尔值决定要构造哪一种Sync实例。

读写锁比ReentrantLock多出了两个内部类:ReadLock和WriteLock, 用来定义读锁和写锁,然后在构造函数中,会构造一个读锁和一个写锁实例保存到成员变量 readerLock 和 writerLock。

需要注意的是:读锁和写锁实例是共享一个AQS,也就是说,共享一个FIFO队列。 这也是实现读写之间相互影响的关键

2.2 读写锁

我们还是先回顾下lock()的内容:

方法名称 描述
void lock() 获取锁. 成功则向下运行,失败则阻塞
void lockInterruptibly() throws InterruptedException 可中断地获取锁,在当前线程获取锁的过程中可以响应中断信号
boolean tryLock() 尝试非阻塞获取锁,调用方法后立即返回,成功返回true,失败返回false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException 在超时时间内获取锁,到达超时时间将返回false,也可以响应中断
void unlock(); 释放锁
Condition newCondition(); 获取等待通知组件实现信号控制,等待通知组件实现类似于Object.wait()方法的功能
  • 共享式读锁
    • 内部调用共享式AQS操作,因此真实的实现就是Sync的 acquireShared 和 releaseShared
	public static class ReadLock implements Lock, java.io.Serializable {
        private static final long serialVersionUID = -5992448646407690164L;
        private final Sync sync;
		
		 protected ReadLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
        }
		
		public void lock() {
            sync.acquireShared(1);
        }
		public void lockInterruptibly() throws InterruptedException {
            sync.acquireSharedInterruptibly(1);
        }
		public boolean tryLock() {
            return sync.tryReadLock();//非阻塞式尝试获取读锁
        }
		public boolean tryLock(long timeout, TimeUnit unit)
                throws InterruptedException {
            return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
        }
		public void unlock() {
            sync.releaseShared(1);
        }
		public Condition newCondition() {
            throw new UnsupportedOperationException();
        }
	}
  • 独享式写锁
    • 内部调用独享式AQS操作,因此真实的实现就是Sync的 acquire 和 release
    public static class WriteLock implements Lock, java.io.Serializable {
        private static final long serialVersionUID = -4992448646407690164L;
        private final Sync sync;
		
		protected WriteLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
        }
		
		public void lock() {
            sync.acquire(1);
        }
		public void lockInterruptibly() throws InterruptedException {
            sync.acquireInterruptibly(1);
        }
		public boolean tryLock() {
            return sync.tryWriteLock();//非阻塞式尝试获取写锁
        }
		public boolean tryLock(long timeout, TimeUnit unit)
                throws InterruptedException {
            return sync.tryAcquireNanos(1, unit.toNanos(timeout));
        }
		public void unlock() {
            sync.release(1);
        }
		public Condition newCondition() {
            return sync.newCondition();
        }
		
		
		public boolean isHeldByCurrentThread() {
            return sync.isHeldExclusively();
        }
		public int getHoldCount() {
            return sync.getWriteHoldCount();
        }
	}

我们可以看到基本核心的代码还是在AQS中实现

2.3 AQS的实现

在上篇我们已经讲到了,AQS需要重写的钩子方法:

方法名称 描述
boolean tryAcquire(int arg) 独占式尝试获取同步状态(通过CAS操作设置同步状态),如果成功返回true,反之返回false
boolean tryRelease(int arg) 独占式释放同步状态,成功返回true,失败返回false。
int tryAcquireShared(int arg) 共享式的获取同步状态,返回大于等于0的值,表示获取成功,反之失败。
boolean tryReleaseShared(int arg) 共享式释放同步状态,成功返回true,失败返回false。
boolean isHeldExclusively() 判断同步器是否在独占模式下被占用,一般用来表示同步器是否被当前线程占用

2.3.1 state的改变

之前在ReentrantLock中,我们知道锁的状态是保存在Sync实例的state字段中的(继承自父类AQS): 0代表无锁状态; >0时代表有锁,具体值为重入次数

现在有了读写两把锁,然而可以看到还是只有一个Sync实例,那么一个Sync实例的state是如何同时保存两把锁的状态的呢?

答案就是用了位分隔:

abstract static class Sync extends AbstractQueuedSynchronizer {
		static final int SHARED_SHIFT   = 16;
        static final int SHARED_UNIT    = (1 << SHARED_SHIFT); //每次要让共享锁+1,就应该让state加 1<<16
		
        static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;  //每种锁的最大重入数量
        static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

        /** 要获取共享锁当前的重入数量 */
        static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
        /** 获取独占锁当前的重入数量  */
        static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
		
		
		private transient Thread firstReader;
        private transient int firstReaderHoldCount;

        Sync() {
            readHolds = new ThreadLocalHoldCounter();
            setState(getState()); // ensures visibility of readHolds
        }

}

state字段是32位的int,读写锁用state的低16位保存写锁(独占锁)的状态;高16位保存读锁(共享锁)的状态

因此:

  1. 要获取独占锁当前的重入数量,就是 state & ((1 << 16) -1) (即 exclusiveCount 方法)
  2. 要获取共享锁当前的重入数量,就是 state >>> 16 (即 sharedCount 方法)

2.3.2 独享式写锁对应的AQS

独享式写锁内部调用独享式AQS操作,因此真实的实现就是Sync的 acquire 和 release。 对应的式AQS的钩子函数 tryAcquiretryRelease

2.3.2.1 独享式写锁的获取

abstract static class Sync extends AbstractQueuedSynchronizer {
	protected final boolean tryAcquire(int acquires) {
				Thread current = Thread.currentThread();
				
				int c = getState();//获取锁状态
				int w = exclusiveCount(c); //获取独占锁的重入数
				
				// 【实现重入】当前state不为0(已被读或写占据当前锁)
				if (c != 0) { 
					if (w == 0 || current != getExclusiveOwnerThread())//如果写锁重入为0说明读锁此时被占用, 或者不被当前写线程占据,则返回false;
						return false;
					if (w + exclusiveCount(acquires) > MAX_COUNT)
						throw new Error("Maximum lock count exceeded"); //写锁重入数溢出
					// Reentrant acquire
					setState(c + acquires); //走到这里标识,被写锁占据且时当前线程占据,重入数增加
					return true;
				}
				
				//【实现抢占】到这里了说明state为0(没有被占用)。
				if (writerShouldBlock() ||  //writerShouldBlock是为了实现公平或非公平策略的
					!compareAndSetState(c, c + acquires))//则尝试通过CAS来抢占,抢占成功,直接返回true
					return false;
					
				setExclusiveOwnerThread(current);
				return true;
			}
			
	}
}

static final class NonfairSync extends Sync {

	//不公平锁一直返回 fasle, 从而直接诱发CAS抢占
	final boolean writerShouldBlock() {
            return false; // writers can always barge
    }
}		

static final class NonfairSync extends Sync {

	//公平锁则判断是否由前置阻塞的节点, 从而判断是否诱发CAS抢占。 
	final boolean writerShouldBlock() {
			return hasQueuedPredecessors();
	}
}	
  • writerShouldBlock 实现公平或非公平策略的
    • 不公平锁一直返回 fasle, 从而直接诱发CAS抢占
    • 公平锁则判断是否由前置阻塞的节点, 从而判断是否诱发CAS抢占。
      • true标识有前置阻塞节点,则tryAcquire直接返回false
      • false标识没有前置阻塞节点,诱发CAS抢占。

2.3.2.2 独享式写锁的释放

abstract static class Sync extends AbstractQueuedSynchronizer {
	protected final boolean tryRelease(int releases) {
				if (!isHeldExclusively())
					throw new IllegalMonitorStateException();  //非当前线程占据则直接抛异常
					
				int nextc = getState() - releases;//【计算 锁状态】
				
				boolean free = exclusiveCount(nextc) == 0;//作为可重入数,state在大于0时标识重入次数,必须退出对应次数时,才算真正的退出
				if (free) 
					setExclusiveOwnerThread(null); //如果独占模式重入数为0了,说明独占模式被释放
					
				setState(nextc);  //更新锁状态
				return free;
	}
}

2.3.3 共享式读锁对应的AQS

类似于写锁,读锁的lock和unlock的实际实现对应Sync的 tryAcquireShared 和 tryReleaseShared方法。

2.3.3.1 共享式读锁的获取

abstract static class Sync extends AbstractQueuedSynchronizer {

	private transient Thread firstReader;//首个获取读锁的线程
	private transient int firstReaderHoldCount;//首个获取读锁的线程的重入数

	Sync() {
		readHolds = new ThreadLocalHoldCounter();
		setState(getState()); // ensures visibility of readHolds
	}
			
	protected final int tryAcquireShared(int unused) {
				Thread current = Thread.currentThread();
				
				int c = getState();//【1】获取锁状态
				
				//【2】如果独占模式被占且不是当前线程持有,则获取失败
				//表示:如果存在写锁,则当前读锁获取失败
				if (exclusiveCount(c) != 0 &&
					getExclusiveOwnerThread() != current)
					return -1; 
				
				//【3】获取 共享锁重入数
				int r = sharedCount(c);
				
				//【4】如果公平策略没有要求阻塞且重入数没有到达最大值,则直接尝试CAS更新state
				if (!readerShouldBlock() &&
					r < MAX_COUNT &&
					compareAndSetState(c, c + SHARED_UNIT)) {
					
					//【5】成功获取读锁后,内部变量的更新操作
					if (r == 0) {
						//如果r=0, 表示,当前线程为第一个获取读锁的线程。
						firstReader = current;
						firstReaderHoldCount = 1;
					} else if (firstReader == current) {
						//如果第一个获取读锁的对象为当前对象,将firstReaderHoldCount 加一。
						firstReaderHoldCount++;
					} else {
						//当前线程功获取锁后,如果不是第一个获取多锁的线程
						// 将该线程持有锁的次数信息,放入线程本地变量中,方便计算当前线程的重入次数
						HoldCounter rh = cachedHoldCounter;
						if (rh == null || rh.tid != getThreadId(current))
							cachedHoldCounter = rh = readHolds.get();
						else if (rh.count == 0)
							readHolds.set(rh);
						rh.count++;
					}
					
					return 1;
				}
				
				return fullTryAcquireShared(current); //用来处理CAS没成功的情况,逻辑和上面的逻辑是类似的,就是加了无限循环
	}
}

static final class NonfairSync extends Sync {

	 final boolean readerShouldBlock() {
		//不公平锁一直返回 fasle, 从而直接诱发CAS抢占
		return apparentlyFirstQueuedIsExclusive();
	 }
	 
	 //这个方法判断队列的head.next是否正在等待独占锁(写锁)。这个方法的意思是:读锁不应该让写锁始终等待。
	 //该方法如果头节点不为空,并头节点的下一个节点不为空,并且不是共享模式【独占模式,写锁】、并且线程不为空,则返回true。
	 final boolean apparentlyFirstQueuedIsExclusive() {
		Node h, s;
		return (h = head) != null &&
			(s = h.next)  != null &&
			!s.isShared()         &&
			s.thread != null;
	 }

}

static final class FairSync extends Sync {
	//公平锁则判断是否由前置阻塞的节点, 从而判断是否诱发CAS抢占。 
	final boolean readerShouldBlock() {
		return hasQueuedPredecessors();
	}
}

我们主要看下CAS更新成功后(获取读锁成功)的操作:

在firstReaderHoldCount中或readHolds(ThreadLocal类型的)的本线程副本中记录当前线程重入数(浅蓝色代码),这是为了实现jdk1.6中加入的getReadHoldCount()方法的

这个方法能获取当前线程重入共享锁的次数(state中记录的是多个线程的总重入次数)

加入了这个方法让代码复杂了不少,但是其原理还是很简单的:如果当前只有一个线程的话,还不需要动用ThreadLocal,直接往firstReaderHoldCount这个成员变量里存重入数,当有第二个线程来的时候,就要动用ThreadLocal变量readHolds了,每个线程拥有自己的副本,用来保存自己的重入数。

fullTryAcquireShared如果CAS失败或readerShouldBlock方法返回true,我们调用fullTryAcquireShared方法继续试图获取读锁。fullTryAcquireShared方法是tryAcquireShared方法的完整版,或者叫升级版,它处理了CAS失败的情况和readerShouldBlock返回true的情况。

final int fullTryAcquireShared(Thread current) {
            HoldCounter rh = null;
            for (;;) {
				//【1】获取锁状态
                int c = getState();
				
                if (exclusiveCount(c) != 0) {
                    if (getExclusiveOwnerThread() != current)
					//【2】如果独占模式被占且不是当前线程持有,则获取失败
				    //表示:如果存在写锁,则当前读锁获取失败
                        return -1;
                } else if (readerShouldBlock()) {
                    //【3】如果没有线程正在持有写锁,则调用readerShouldBlock检测根据公平原则,当前线程是否应该进入等待队列。
                    if (firstReader == current) {
                        // assert firstReaderHoldCount > 0;
                    } else {
                        if (rh == null) {
                            rh = cachedHoldCounter;
                            if (rh == null || rh.tid != getThreadId(current)) {
                                rh = readHolds.get();
                                if (rh.count == 0)
                                    readHolds.remove();
                            }
                        }
                        if (rh.count == 0)
                            return -1;
                    }
                }
				
				//共享锁溢出
                if (sharedCount(c) == MAX_COUNT)
					
                    throw new Error("Maximum lock count exceeded");
				
				//尝试CAS更新state
                if (compareAndSetState(c, c + SHARED_UNIT)) {
                    if (sharedCount(c) == 0) {
                        firstReader = current;
                        firstReaderHoldCount = 1;
                    } else if (firstReader == current) {
                        firstReaderHoldCount++;
                    } else {
                        if (rh == null)
                            rh = cachedHoldCounter;
                        if (rh == null || rh.tid != getThreadId(current))
                            rh = readHolds.get();
                        else if (rh.count == 0)
                            readHolds.set(rh);
                        rh.count++;
                        cachedHoldCounter = rh; // cache for release
                    }
                    return 1;
                }
            }
}

2.3.3.2 共享式读锁的释放


protected final boolean tryReleaseShared(int unused) {
            Thread current = Thread.currentThread();
            //下边代码也是为了实现jdk1.6中加入的getReadHoldCount()方法,在更新当前线程的重入数。
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
                if (firstReaderHoldCount == 1)
                    firstReader = null;
                else
                    firstReaderHoldCount--;
            } else {
                HoldCounter rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                int count = rh.count;
                if (count <= 1) {
                    readHolds.remove();
                    if (count <= 0)
                        throw unmatchedUnlockException();
                }
                --rh.count;
            }
			
            //这里是真正的释放同步状态的逻辑,就是直接同步状态-SHARED_UNIT,然后CAS更新,没啥好说的
            for (;;) {
                int c = getState();
                int nextc = c - SHARED_UNIT;
                if (compareAndSetState(c, nextc))
                    // Releasing the read lock has no effect on readers,
                    // but it may allow waiting writers to proceed if
                    // both read and write locks are now free.
                    return nextc == 0;
            }
        }
  • 共享式锁的【更新锁状态】不能通过setState,而是通过CAS操作。 这是由于独享式锁释放时肯定是单线程模型的,而共享式的则可能是多线程模型
  • 不需要调用setExclusiveOwnerThread 设置独占状态

三、补充内容

通过上面的源码分析,我们可以发现一个现象:

  1. 在线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的时候,如果发现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)
  2. 在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)

细想想,这个设计是合理的:因为

  1. 当线程获取读锁的时候,可能有其他线程同时也在持有读锁,因此不能把获取读锁的线程“升级”为写锁;
  2. 而对于获得写锁的线程,它一定独占了读写锁,因此可以继续让它获取读锁
    • 当它同时获取了写锁和读锁后,还可以先释放写锁继续持有读锁,这样一个写锁就“降级”为了读锁。

综上:

  • 一个线程要想同时持有写锁和读锁,必须先获取写锁再获取读锁;
  • 写锁可以“降级”为读锁;
  • 读锁不能“升级”为写锁。

猜你喜欢

转载自blog.csdn.net/fei20121106/article/details/83268588