看过我上一博文《大厂面试必会AQS(1)——从ReentrantLock源码认识AQS》了解到可重入锁ReentrantLock是排他锁,排他锁在同一时刻仅有一个线程可以进行访问,但是在大多数场景下,大部分时间都是提供读服务,而写服务占有的时间较少。然而读服务不存在数据竞争问题,如果一个线程在读时禁止其他线程读势必会导致性能降低。所以就提供了读写锁。
读写锁维护了一对锁,一个读锁一个写锁,通过分离读锁和写锁来提高并发能力
可以看到ReadWriteLock 接口只提供了读锁和写锁获取这两个方法,本文通过ReentrantReadWriteLock来认识读写锁
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
说到底,读写锁的代码不复杂,难点终究还是AQS,接下来接着上一文章研究一下AQS的共享式同步状态的获取与释放,我们知道AQS通过int变量state来表示锁被一个线程重复获取的次数,那么读写锁的实现关键即是怎么在一个int变量上维护多种状态——按位切割,高16位表示读,低16位表示写,通过位运算获取读锁或者写锁的状态,这里有一个逻辑会用在源码中:如果State不等于0,而写状态等于0(写状态是低16位,只需将state&0X0000FFFF抹去高16位即可得到),那么读状态大于0(读状态为高16位,State无符号位移16位即可获取),即读锁已经被获得。
1.读锁获取
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
读锁的加锁调用acquireShared,仍然是首先尝试获取锁(以共享式)
源码如下
protected final int tryAcquireShared(int unused) {
/*
* Walkthrough:
* 1. If write lock held by another thread, fail.
* 2. Otherwise, this thread is eligible for
* lock wrt state, so ask if it should block
* because of queue policy. If not, try
* to grant by CASing state and updating count.
* Note that step does not check for reentrant
* acquires, which is postponed to full version
* to avoid having to check hold count in
* the more typical non-reentrant case.
* 3. If step 2 fails either because thread
* apparently not eligible or CAS fails or count
* saturated, chain to version with full retry loop.
*/
Thread current = Thread.currentThread();
int c = getState();
//写锁被别的线程持有返回-1
if (exclusiveCount(c) != 0 &&
//保证获得写锁的线程可以再次获得读锁,避免死锁
getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount(c);
//是否需要阻塞
if (!readerShouldBlock() &&
//持有线程小于最大数(65535)
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
//r == 0,表示第一个读锁线程,第一个读锁firstRead是不会加入到readHolds中
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {// 当前线程为第一个读线程,表示第一个读锁线程重入
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
// 如果最后一个线程计数器是 null 或者不是当前线程,那么就新建一个 HoldCounter 对象
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get(); // 给当前线程新建一个 HoldCounter
else if (rh.count == 0)
// 如果不是 null,最后一个获取读锁的线程是当前线程,且 count 是 0,就将上个线程的 HoldCounter 覆盖本地的。
readHolds.set(rh);
//计数器加1
rh.count++;
}
return 1;
}
// 死循环获取读锁。包含锁降级策略。
return fullTryAcquireShared(current);
}
注意:更新成功后会在firstReaderHoldCount中或readHolds(ThreadLocal类型的)的本线程副本中记录当前线程重入数,这是为了实现jdk1.6中加入的getReadHoldCount()方法的,这个方法能获取当前线程重入共享锁的次数(state中记录的是多个线程的总重入次数),加入了这个方法让代码复杂了不少,但是其原理还是很简单的:如果当前只有一个线程的话,还不需要动用ThreadLocal,直接往firstReaderHoldCount这个成员变量里存重入数,当有第二个线程来的时候,就要动用ThreadLocal变量readHolds了,每个线程拥有自己的副本,用来保存自己的重入数。
分析:相比ReentrantLock中看到的tryAcquireShared方法,出现了几个陌生的方法:
1.exclusiveCount:获得写状态
//c即为state c & EXCLUSIVE_MASK 即可获得低16位的状态,即写状态
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
//将1左移16位-1得到 0X0000FFFF
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
static final int SHARED_SHIFT = 16;
2.sharedCount: 获得读状态
//c 无符号位移16位获得高16位 即读状态
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
static final int SHARED_SHIFT = 16;
3.readerShouldBlock读是否应该被阻塞
公平锁会考虑前面是不是有已经等待的线程(不管是获取读锁还是写锁)
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
非公平锁则只关注第一个等待节点是不是获取写锁
final boolean readerShouldBlock() {
/* As a heuristic to avoid indefinite writer starvation,
* block if the thread that momentarily appears to be head
* of queue, if one exists, is a waiting writer. This is
* only a probabilistic effect since a new reader will not
* block if there is a waiting writer behind other enabled
* readers that have not yet drained from the queue.
*/
return apparentlyFirstQueuedIsExclusive();
}
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
4.cachedHoldCounter
保存的是最后一个成功获取到读锁的线程,每当有新的线程获取到读锁,这个变量都会更新。这个变量的目的是:当最后一个获取读锁的线程重复获取读锁,或者释放读锁,就会直接使用这个变量,速度更快,相当于缓存。
/**
* The hold count of the last thread to successfully acquire
* readLock.
*/
private transient HoldCounter cachedHoldCounter;
5.firstReader
private transient Thread firstReader = null;
private transient int firstReaderHoldCount;
firstReader是获取读锁的第一个线程。如果只有一个线程获取读锁,很明显,使用这样一个变量速度更快。firstReaderHoldCount是firstReader的计数器
6.readHolds
当前线程持有的可重入读锁的数量
/**
* The number of reentrant read locks held by current thread.
* Initialized only in constructor and readObject.
* Removed whenever a thread's read hold count drops to 0.
*/
private transient ThreadLocalHoldCounter readHolds;
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
static final class HoldCounter {
int count = 0;
// Use id, not reference, to avoid garbage retention
final long tid = getThreadId(Thread.currentThread());
}
最开始时readHolds也没有值,readHolds.get()方法会导致ThreadLocalHoldCounter类initialValue方法执行,也就会返回count=0以及tid为当前线程tid的属性。当下次该线程再次获取读锁时将会执行( **当前线程曾经获取到读锁并释放了,再次获取读锁,且期间没有任何其他线程获取 读锁!!! **)
else if (rh.count == 0)
readHolds.set(rh);
如果有几个线程同时竞争,可能导致compareAndSetState失败进入fullTryAcquireShared自旋获取到锁(CAS 设置失败,或者队列有等待的线程(公平锁)情况下)
final int fullTryAcquireShared(Thread current) {
/*
* This code is in part redundant with that in
* tryAcquireShared but is simpler overall by not
* complicating tryAcquireShared with interactions between
* retries and lazily reading hold counts.
*/
HoldCounter rh = null;
for (;;) {
int c = getState();
//存在写互斥锁且互斥锁的拥有者不是当前线程则放弃尝试自旋
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
// else we hold the exclusive lock; blocking here
// would cause deadlock.
// 如果写锁空闲,且可以获取读锁。
} else if (readerShouldBlock()) {
// Make sure we're not acquiring read lock reentrantly
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;
}
}
// 如果读锁次数达到 65535 ,抛出异常
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 尝试对 state 加 65536, 也就是设置读锁,实际就是对高16位加一。
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;
}
}
}
当tryAcquireShared方法尝试失败后将进入doAcquireShared获取读锁,下面看下doAcquireShared该方法内部到底做了些什么:
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
//成功获取到读共享锁,那么将唤醒所有其他读锁,不唤醒写互斥锁线程!!!
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
//获取读锁失败,进入阻塞等待被唤醒
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
可以看到,如果前驱节点是头结点,则尝试获取读锁,成功的话就唤醒其它读锁
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
/*
* Try to signal next queued node if:
* Propagation was indicated by caller,
* or was recorded (as h.waitStatus either before
* or after setHead) by a previous operation
* (note: this uses sign-check of waitStatus because
* PROPAGATE status may transition to SIGNAL.)
* and
* The next node is waiting in shared mode,
* or we don't know, because it appears null
*
* The conservatism in both of these checks may cause
* unnecessary wake-ups, but only when there are multiple
* racing acquires/releases, so most need signals now or soon
* anyway.
*/
/*
* propagate>0,说明还有资源
* h.waitStatus < 0 h的waitStatus要么是-1,要么是-3,
* 满足以上2个条件之一,就能进入doReleaseShared(),释放后继节点了
*/
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
private void doReleaseShared() {
/*
*会先判断head节点的waitStatus,doReleaseShared只被2个方法调用,1个是setHeadAndPropagate,1个是releaseShared,我只讨论setHeadAndPropagate方法调用的情况
*所以进来的head的waitStatus也只有3种可能,0,-1,-3.
*如果是0的情况说明还有资源,但是后继节点为空,则会把waitStatus设置成-3,便结束这个方法.当节点释放锁的时候,会执行releaseShared,如果这个时候waitStatus还是0,说明后面还是没节点,如果后面有节点,则必定会更改waitStatus为-1
*如果是-1,则会更改成0并且唤醒后继节点
*/
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
2.读锁释放
读锁释放相对简单
public void unlock() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) { // 释放成功后,如果是 0,表示读锁和写锁都空闲,则可以唤醒后面的等待线程
doReleaseShared();
return true;
}
return false;
}
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) {//当前线程是第一个读线程
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)//进入锁的次数为1,把读线程置null(后续代码会将读状态再减1<<16)
firstReader = null;
else
firstReaderHoldCount--;
} else {
HoldCounter rh = cachedHoldCounter;//最后一个获得读锁的线程id及计数器
if (rh == null || rh.tid != getThreadId(current))//最后一个获得读锁的线程不是当前线程,新建一个HoldCounter
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;//1 << 16
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.
// 修改成功后,如果是 0,表示读锁和写锁都空闲,则可以唤醒后面的等待线程
return nextc == 0;
}
}
tryReleaseShared方法将读状态置0,doReleaseShared方法负责唤醒后继节点,与setHeadAndPropagate方法中被调用的逻辑一致
看下WriteLock类中的lock和unlock方法:
1.加锁
public void lock() {
sync.acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
可以看到就是调用的独占式同步状态的获取,因此真实的实现就是Sync的 tryAcquire
protected final boolean tryAcquire(int acquires) {
/*
* Walkthrough:
* 1. If read count nonzero or write count nonzero
* and owner is a different thread, fail.
* 2. If count would saturate, fail. (This can only
* happen if count is already nonzero.)
* 3. Otherwise, this thread is eligible for lock if
* it is either a reentrant acquire or
* queue policy allows it. If so, update state
* and set owner.
*/
Thread current = Thread.currentThread();
int c = getState();//获取状态
int w = exclusiveCount(c);//写状态
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0)
// 当前state不为0,此时:如果写锁状态为0说明读锁此时被占用返回false;
// 如果写锁状态不为0且写锁没有被当前线程持有返回false
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)//如果写锁已经被获取,若写锁获得次数超过最大值抛出异常
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
//更新状态
//此时当前线程已持有写锁,现在是重入,所以只需要修改锁的数量即可。
setState(c + acquires);
return true;
}
//到这里说明此时c=0,读锁和写锁都没有被获取
//写锁获得是否应该阻塞,公平锁需判断有没有前驱节点,非公平锁直接返回false
if (writerShouldBlock() ||
//更新状态
!compareAndSetState(c, c + acquires))
return false;
//获得写锁成功并设置当前线程为拥有者
setExclusiveOwnerThread(current);
return true;
}
过程较为简单,只需注意写锁的获得必须是读锁没有被占用
2.写锁的释放
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
//先调用tryRelease方法
if (tryRelease(arg)) {
Node h = head;
//唤醒后继节点
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
//当前线程是否为写锁拥有线程,不是则抛出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//写锁的新线程数
int nextc = getState() - releases;
//写锁重入数减一后是否为0
boolean free = exclusiveCount(nextc) == 0;
if (free)
// 写锁拥有的线程置null
setExclusiveOwnerThread(null);
//设置写锁的新线程数
//不管独占模式是否被释放,更新独占重入数
setState(nextc);
return free;
}
可以看到首先查看当前线程是否为写锁的持有者,如果不是抛出异常。然后检查释放后写锁的线程数是否为0,如果为0则表示写锁空闲了,释放锁资源将锁的持有线程设置为null,否则释放仅仅只是一次重入锁而已,并不能将写锁的线程清空。
什么是锁降级?
锁重入还允许从写入锁降级为读取锁,其实现方式是:先获取写入锁,然后获取读取锁,最后释放写入锁。注意:先释放写锁再获取读锁不是锁降级!!!
代码体现:在tryAcquireShared,fullTryAcquireShared等方法中存在如下代码
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
分析:当写锁被持有时,如果持有该锁的线程不是当前线程,就返回 “获取锁失败”,反之就会继续获取读锁。称之为锁降级。
锁降级的好处
线程1先释放写锁,再获得读锁的时间差里,数据存在被其他线程修改的可能,如果先获得读锁在释放写锁,那么其他线程想要获得写锁必须等待线程1释放读锁,另外锁降级获得读锁的过程是不需要参与资源竞争。
有没有锁升级?
答案是没有,因为写锁是共享的,线程1在读的同时,别的线程也在读取数据,而且在写锁的获取源码中,若当前存在读锁,则获取失败。