文章目录
- 一、读写锁介绍
- 二、ReentrantReadWriteLock底层原理
- 1. 读写锁的设计
一、读写锁介绍
现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁(读多写少)。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源(读读可以并发);但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写操作了(读写,写读,写写互斥)。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。
针对这种场景,JAVA的并发包提供了读写锁ReentrantReadWriteLock,它内部,维护了 一对相关的锁,一个用于只读操作,称为读锁;一个用于写入操作,称为写锁。
线程进入读锁的前提条件:
- 没有其它线程的写锁
- 没有写请求或者有写请求,但是调用线程和持有锁的线程是同一个
线程进入写锁的前提条件:
- 没有其他线程的读锁
- 没有其他线程的写锁
读写锁有以下三个重要的特性:
- 公平选择性:支持非公平和公平的锁获取方式,吞吐量还是非公平优于公平
- 可重入:读锁和写锁都支持线程重入,以读写线程为例,读锁获取读锁后,能够再次获取读锁,写线程在获取写锁之后能够再次获取写锁,同时也可以获取读锁。
- 锁降级:遵循获取写锁,再获取读锁最后释放写锁的次序,写锁能够降级为读锁。
二、ReentrantReadWriteLock底层原理
看源码需要了解三个核心问题:
读写锁是怎样实现分别记录读写状态的?
写锁时怎么获取和释放的?
读锁时怎么获取和释放的?
1. 读写锁的设计
首先看它的类信息:
public class ReentrantReadWriteLockimplements ReadWriteLock, java.io.Serializable {}
可以发现该类实现了ReadWriteLock这个接口
public interface ReadWriteLock {Lock readLock();Lock writeLock();
}
该接口就实现了读锁和写锁的规范,以下就是相关的类图
下面看看ReentrantReadWriteLock的读写锁的实现逻辑,首先看写锁:
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();}public String toString() {int r = sync.getReadLockCount();return super.toString() +"[Read locks = " + r + "]";}}
- ReadLock:是一个ReetrantReadWriteLock的静态内部类
- Sync:和ReentrantLock一样,Sync也是ReetrantReadWriteLock的一个静态内部抽象类,它继承了AbstractQueuedSynchronizer,实现了AQS的逻辑,然后它会有两种实现,分别是FairSync公平锁,和NonfairSync非公平锁
- lock:可以发现加读锁加的是AQS的共享锁
- tryLock:尝试获取读锁
然后看看写锁是怎么实现的
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 String toString() {Thread o = sync.getOwner();return super.toString() + ((o == null) ?"[Unlocked]" :"[Locked by thread " + o.getName() + "]");}public boolean isHeldByCurrentThread() {return sync.isHeldExclusively();}public int getHoldCount() {return sync.getWriteHoldCount();}}
下面我们需要思考一个核心问题,读写锁的状态是怎么用AQS底层的state状态来维护的。其实这里的核心问题就是,如何用一个变量维护多种状态。在 ReentrantLock 中,使用 Sync ( 实际是 AQS )的 int 类型的 state 来表示同步状态,表示锁被一个线程重复获取的次数。但是,读写锁 ReentrantReadWriteLock 内部维护着一对读写锁,如果要用一个变量维护多种状态,需要采用“按位切割使用”的方式来维护这个变量,将其切分为两部分:高16为表示读,低16为表示写。分割之后,读写锁是如何迅速确定读锁和写锁的状态呢? 其实底层是通过位运算来实现的。假如当前同步状态为S, 那么写状态,等于 S & 0x0000FFFF(将高 16 位全部抹去)。 当写状态加1,等于S+1。读状态,等于 S >>> 16 (无符号补 0 右移 16 位)。当读状态加1,等于 S+(1<<16),也就是S+0x00010000。根据状态的划分能得出一个推论:S不等于0时,当写状态(S&0x0000FFFF)等于0时,则读状态(S>>>16)大于0,即读锁已被获取。
源码如下:
//该部分在Sync类中static final int SHARED_SHIFT = 16;static final int SHARED_UNIT = (1 << SHARED_SHIFT);static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;/** Returns the number of shared holds represented in count */static int sharedCount(int c) { return c >>> SHARED_SHIFT; }/** Returns the number of exclusive holds represented in count */static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
- exclusiveCount:获得持有写状态的锁的次数
- sharedCount:获得持有读状态的锁的线程数量,不同于写锁,读锁利用同时被多个线程持有,而每个线程持有的读锁支持重入的特性,所以需要每个线程持有的读锁数量单独计数,这就需要HoldCounter计数器。
HoldCounter计数器
读锁的内在机制其实就是一个共享锁。一次共享锁的操作就相当于对HoldCounter 计数器的操作。获取共享锁,则该计数器 + 1,释放共享锁,该计数器 - 1。只有当线程获取共享锁后才能对共享锁进行释放、重入操作。
static final class HoldCounter {int count = 0;// Use id, not reference, to avoid garbage retentionfinal long tid = getThreadId(Thread.currentThread());}static final class ThreadLocalHoldCounterextends ThreadLocal<HoldCounter> {public HoldCounter initialValue() {return new HoldCounter();}}
写锁的获取
写锁是一个支持重进入的排它锁。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程, 则当前线程进入等待状态。
protected final boolean tryAcquire(int acquires) {
//获取当前线程Thread current = Thread.currentThread();//获取当前state的值int c = getState();//获取写锁的重入次数int w = exclusiveCount(c);//state!=0则表示当前有写锁或读锁if (c != 0) {//判断是否是重入if (w == 0 || current != getExclusiveOwnerThread())return false;if (w + exclusiveCount(acquires) > MAX_COUNT)throw new Error("Maximum lock count exceeded");//获取重入锁setState(c + acquires);return true;}// writerShouldBlock有公平与非公平的实现, 非公平返回false,会尝试通过cas加锁,c==0 写锁未被任何线程获取,当前线程是否阻塞或者cas尝试获取锁if (writerShouldBlock() ||!compareAndSetState(c, c + acquires))return false;//设置锁由当前线程独占setExclusiveOwnerThread(current);return true;}
上面简单的代码就实现了下面的逻辑:
- 读写互斥
- 写写互斥
- 写锁支持同一个线程重入
- writeShouldBlock写锁是否阻塞实现取决公平与非公平的策略
写锁的释放
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);setState(nextc);return free;}
读锁的获取
protected final int tryAcquireShared(int unused) {//获取当前线程Thread current = Thread.currentThread();//获取stateint c = getState();//exclusiveCount(c) != 0 判断是否有写锁//getExclusiveOwnerThread() != current),判断当前线程是否是写锁的持有者if (exclusiveCount(c) != 0 &&getExclusiveOwnerThread() != current)return -1;int r = sharedCount(c);if (!readerShouldBlock() &&r < MAX_COUNT &&//cas加读锁compareAndSetState(c, c + SHARED_UNIT)) {//r==0表示第一次获取读锁if (r == 0) {//设置第一个读为当前线程firstReader = current;//设置当前读锁的重入次数firstReaderHoldCount = 1;} else if (firstReader == current) { //第一个读的重入firstReaderHoldCount++;} else {//若不是第一个读,则用HoldCounter记录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);//第一次读锁获取失败,再次尝试(fullTryAcquireShared)}
上面代码的逻辑就实现了:
- 读锁共享,读读不互斥
- 读锁可重入,每个获取读锁的线程都会记录对应的重入数
- 读写互斥,锁降级场景除外
- 支持锁降级,持有写锁的线程,可以获取读锁,但是后续要记得把读锁和写锁读释放
- readerShouldBlock读锁是否阻塞实现取决公平与非公平的策略(FairSync和NonfairSync)
读锁的释放
protected final boolean tryReleaseShared(int unused) {Thread current = Thread.currentThread();if (firstReader == current) {// assert firstReaderHoldCount > 0;if (firstReaderHoldCount == 1)firstReader = null;elsefirstReaderHoldCount--;} 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;}for (;;) {int c = getState();int nextc = c - SHARED_UNIT;if (compareAndSetState(c, nextc))return nextc == 0;}}