文章目录
我们在介绍AbstractQueuedSynchronizer的时候介绍过,AQS支持独占式同步状态获取/释放、共享式同步状态获取/释放两种模式,对应的典型应用分别是ReentrantLock和Semaphore
AQS还可以混合两种模式使用,读写锁ReentrantReadWriteLock就是如此。
设想以下情景:我们在系统中有一个多线程访问的缓存,多个线程都可以对缓存进行读或写操作,但是读操作远远多于写操作,要求写操作要线程安全,且写操作执行完成要求对当前的所有读操作马上可见。
分析上面的需求:
- 因为有多个线程可能会执行写操作,因此多个线程的写操作必须同步串行执行;
- 而写操作执行完成要求对当前的所有读操作马上可见,这就意味着当有线程正在读的时候,要阻塞写操作,当正在执行写操作时,要阻塞读操作。
一个简单的实现就是将数据直接加上互斥锁,同一时刻不管是读还是写线程,都只能有一个线程操作数据。但是这样的问题就是如果当前只有N个读线程,没有写线程,这N个读线程也要傻呵呵的排队读,尽管其实是可以安全并发提高效率的。
因此理想的实现是:
- 当有写线程时,则写线程独占同步状态。
- 当没有写线程时只有读线程时,则多个读线程可以共享同步状态。
读写锁就是为了实现这种效果而生。
一、使用示例
我们先来看一下读写锁怎么使用,这里我们基于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。从代码可以看出:
- 我们先创建一个 ReentrantReadWriteLock 对象,构造函数 false 代表是非公平的(非公平的含义和ReentrantLock相同)。
- 然后通过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位保存读锁(共享锁)的状态。
因此:
- 要获取独占锁当前的重入数量,就是 state & ((1 << 16) -1) (即 exclusiveCount 方法)
- 要获取共享锁当前的重入数量,就是 state >>> 16 (即 sharedCount 方法)
2.3.2 独享式写锁对应的AQS
独享式写锁内部调用独享式AQS操作,因此真实的实现就是Sync的 acquire 和 release。 对应的式AQS的钩子函数 tryAcquire
和 tryRelease
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抢占。
- true标识有前置阻塞节点,则
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 设置独占状态
三、补充内容
通过上面的源码分析,我们可以发现一个现象:
- 在线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的时候,如果发现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)
- 在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)
细想想,这个设计是合理的:因为
- 当线程获取读锁的时候,可能有其他线程同时也在持有读锁,因此不能把获取读锁的线程“升级”为写锁;
- 而对于获得写锁的线程,它一定独占了读写锁,因此可以继续让它获取读锁
- 当它同时获取了写锁和读锁后,还可以先释放写锁继续持有读锁,这样一个写锁就“降级”为了读锁。
综上:
- 一个线程要想同时持有写锁和读锁,必须先获取写锁再获取读锁;
- 写锁可以“降级”为读锁;
- 读锁不能“升级”为写锁。