碎碎念)

花了两天时间,终于把ReentrantReadWriteLock(读写锁)解析做完了。之前钻研过AQS(AbstractQueuedSynchronizer)的源码,弄懂读写锁也没有想象中那么困难。而且阅读完ReentrantReadWriteLock的源码,正好可以和AQS的源码串起来理解,相辅相成。后面博主会尽快把AQS的源码解析整出来

简介

ReentrantReadWriteLock是一个可重入读写锁,内部提供了读锁写锁的单独实现。其中读锁用于只读操作,可被多个线程共享;写锁用于写操作,只能互斥访问

ReentrantReadWriteLock尤其适合读多写少的应用场景

读多写少:

在一些业务场景中,大部分只是读数据,写数据很少,如果这种场景下依然使用独占锁(如synchronized),会大大降低性能。因为独占锁会使得本该并行执行的读操作,变成了串行执行

ReentrantReadWriteLock实现了ReadWriteLock接口,该接口只有两个方法,分别用于返回读锁和写锁,这两个锁都是Lock对象。该接口源码如下:

public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}

ReentrantReadWriteLock有两个域,分别存放读锁和写锁:

private final ReentrantReadWriteLock.ReadLock readerLock;

private final ReentrantReadWriteLock.WriteLock writerLock;

ReentrantReadWriteLock的核心原理主要在于两点:

  • 内部类Sync:其实现了的AQS大部分方法。Sync类有两个子类FairSync和NonfairSync,分别实现了公平读写锁非公平读写锁。Sync类及其子类的源码解析会在后面逐步给出
  • 内部类ReadLock和WriteLock:分别是读锁和写锁的具体实现,它们都和ReentrantLock一样实现了Lock接口,因此实现的手段也和ReentrantLock一样,都是委托给内部的Sync类对象来实现,对应的源码解析也会在后面给出

ReentrantReadWriteLock的特点

读写锁的互斥关系

  • 读锁和写锁之间是互斥模式:当有线程持有读锁时,写锁不能获得;当有别的线程持有写锁时,读锁不能获得
  • 读锁和读锁之间是共享模式
  • 写锁和写锁之间是互斥模式

可重入性

ReentrantReadWriteLock在ReadWriteLock接口之上,添加了可重入的特性,且读锁和写锁都支持可重入

  • 如果一个线程获取了读锁,那么它可以再次获取读锁(但直接获取写锁会失败,原因见下方的“锁的升降级”)
  • 如果一个线程获取了写锁,那么它可以再次获取写锁或读锁

锁的升降级

锁升级

ReentrantReadWriteLock不支持锁升级,即同一个线程获取读锁后,直接申请写锁是不能获取成功的。测试代码如下:

public class Test1 {
public static void main(String[] args) {
ReentrantReadWriteLock rtLock = new ReentrantReadWriteLock();
rtLock.readLock().lock();
System.out.println("get readLock.");
rtLock.writeLock().lock();
System.out.println("blocking");
}
}

运行到第6行会因为获取失败而被阻塞,导致Test1发生死锁。命令行输出如下:

get readLock.

锁降级

ReentrantReadWriteLock支持锁降级,即同一个线程获取写锁后,直接申请读锁是可以直接成功的。测试代码如下:

public class Test2 {
public static void main(String[] args) {
ReentrantReadWriteLock rtLock = new ReentrantReadWriteLock();
rtLock.writeLock().lock();
System.out.println("writeLock");
rtLock.readLock().lock();
System.out.println("get read lock");
}
}

该程序不会产生死锁。结果输出如下:

writeLock
get read lock Process finished with exit code 0

读写锁的升降级规则总结

  • ReentrantReadWriteLock不支持锁升级,因为可能有其他线程同时持有读锁,而读写锁之间是互斥的,存在冲突‘
  • ReentrantReadWriteLock支持锁降级,因为如果该线程持有写锁时,一定没有其他线程能够持有读锁或写锁的,因此降级为读锁不存在冲突

公平锁和非公平锁

ReentrantReadWriteLock支持公平模式和非公平模式获取锁。从性能上来看,非公平模式更好

二者的规则如下:

  • 公平锁:无论是读线程还是写线程,在申请锁时都会检查是否有其他线程在同步队列中等待。如果有,则让步
  • 非公平锁:如果是读线程,在申请锁时会判断是否有写线程在同步队列中等待。如果有,则让步;如果是写线程,则直接竞争锁资源,不会在乎别的线程

Sync类

Sync是一个抽象类,有两个具体子类NonfairSync和FairSync,分别对应非公平读写锁公平读写锁。Sync类的主要作用就是为这两个子类提供绝绝绝大部分的方法实现

只定义了两个抽象方法writerShouldBlock和readerShouldBlocker交给两个子类去实现,太宠了吧-_-||

读状态和写状态

Sync利用AQS单个state,同时表示读状态和写状态,源码如下:

abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 6317671515068378041L; /*
* Read vs write count extraction constants and functions.
* Lock state is logically divided into two unsigned shorts:
* The lower one representing the exclusive (writer) lock hold count,
* and the upper the shared (reader) hold count.
*/ 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; } // ······ };

根据上面源码可以看出:

  • SHARED_SHIFT表示AQS中的state(int型,32位)的高16位,作为“读状态”,低16位作为“写状态
  • SHARED_UNIT二级制为2^16,读锁加1,state加SHARED_UNIT
  • MAX_COUNT就是写或读资源的最大数量,为2^16-1
  • 使用sharedCount方法获取读状态,使用exclusiveCount方法获取获取写状态

state划分为读、写状态的示意图如下,其中读锁持有1个,写锁持有3个:

记录首个获得读锁的线程

private transient Thread firstReader = null;
private transient int firstReaderHoldCount;

firstReader记录首个获得读锁的线程;firstReaderHoldCount记录其持有的读锁数

线程局部计数器

Sync类定义了一个线程局部变量readHolds,用于保存当前线程重入读锁的次数。如果该线程的读锁数减为0,则将该变量从线程局部域中移除。相关源码如下:

// 内部类,用于记录当前线程重入读锁的次数
static final class HoldCounter {
int count = 0;
// 这里使用线程的id而非直接引用,是为了方便GC
final long tid = getThreadId(Thread.currentThread());
} // 内部类,继承ThreadLocal,该类型的变量是每个线程各自保存一份,其中保存的是HoldCounter对象,用set方法保存,get方法获取
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
} private transient ThreadLocalHoldCounter readHolds;

由于readHolds变量是线程局部变量(继承ThreadLocal类),每个线程都会保存一份副本,不同线程调用其get方法返回的HoldCounter对象不同

readHolds中的HoldCounter变量保存了每个读线程的重入次数,即其持有的读锁数量。这么做的目的是便于线程释放读锁时进行合法性判断:线程在不持有读锁的情况下释放锁是不合法的,需要抛出IllegalMonitorStateException异常

缓存

Sync类定义了一个HoldCounter变量cachedHoldCounter,用于保存最近获取到读锁的线程的重入次数。源码如下:

// 这是一个启发式算法
private transient HoldCounter cachedHoldCounter;

设计该变量的目的是:将其作为一个缓存,加快代码执行速度。因为获取、释放读锁的线程往往都是最近获取读锁的那个线程,虽然每个线程的重入次数都会使用readHolds来保存,但使用readHolds变量会涉及到ThreadLocal内部的查找(lookup),这是存在一定开销的。有了cachedHoldCounter这个缓存后,就不用每次都在ThreadLocal内部查找,加快了代码执行速度。相当于用空间换时间

获取锁

无论是公平锁还是非公平锁,它们获取锁的逻辑都是相同的,因此Sync类在这一层就提供了统一的实现

但是,获取写锁和获取读锁的逻辑不相同:

  • 写锁是互斥资源,获取写锁的逻辑主要在tryAcquire方法
  • 读锁是共享资源,获取读锁的逻辑主要在tryAcquireShared方法

具体的源码分析见下方的“读锁”和“写锁”各自章节的“获取x锁”部分

释放锁

无论是公平锁还是非公平锁,它们释放锁的逻辑都是相同的,因此Sync类在这一层就提供了统一的实现

但是,释放写锁和释放读锁的逻辑不相同:

  • 写锁是互斥资源,释放写锁的逻辑主要在tryRelease方法
  • 读锁是共享资源,释放读锁的逻辑主要在tryReleaseShared方法

具体的源码分析见下方的“读锁”和“写锁”各自章节的“释放x锁”部分

写锁

写锁是由内部类WriteLock实现的,其实现了Lock接口,获取锁、释放锁的逻辑都委托给了Sync类实例sync来执行。WriteLock的基本结构如下:

public static class WriteLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = -4992448646407690164L;
private final Sync sync; // 构造方法注入Sync类对象
protected WriteLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
} // 实现了Lock接口的所有方法
}

获取写锁

WriteLock使用lock方法获取写锁,一次获取一个写锁,源码如下:

public void lock() {
sync.acquire(1);
}

lock方法内部实际调用的是AQS的acquire方法,源码如下:

public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

而acquire方法会调用子类Sync实现的tryAcquire方法,如下:

protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
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;
}
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false; // 如果根据公平性判断此时写线程需要被阻塞,或在获取过程中发生竞争且竞争失败,则获取失败
setExclusiveOwnerThread(current);
return true;
}

分为三步:

1、如果读锁正在被获取中,或者写锁被获取中但不是本线程持有,则获取失败

2、如果获取写锁达到饱和,则抛出异常

3、如果上面两个都不成立,说明此线程可以请求写锁。但需要先根据公平性来判断是否应该先阻塞。如果不用阻塞,且CAS成功,则获取成功。否则获取失败

其中公平性判断所调用的writerShouldBlock,在后面分析公平性锁和非公平性锁时会解析

如果tryAcquire方法获取写锁成功,则acquire方法直接返回,否则进入同步队列阻塞等待

tryAcquire体现的读写锁的特征:

  • 互斥关系:

    • 写锁和写锁之间是互斥的:如果是别的线程持有写锁,那么直接返回false
    • 读锁和写锁之间是互斥的。当有线程持有读锁时,写锁不能获得:如果c!=0且w==0,说明此时有线程持有读锁,直接返回false
  • 可重入性:如果当前线程持有写锁,就不用进行公平性判断writerShouldBlock,请求锁一定会获取成功
  • 不允许锁升级:如果当前线程持有读锁,想要直接申请写锁,此时c!=0且w==0,而exclusiveOwnerThread是null,不等于current,直接返回false

释放写锁

WriteLock使用unlock方法释放写锁,如下:

public void unlock() {
sync.release(1);
}

unlock内部实际上调用的是AQS的release方法,源码如下:

public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}

而该方法会调用子类Sync实现的tryAcquire方法,源码如下:

protected final boolean tryRelease(int releases) {
// 如果并不持有锁就释放,会抛出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException(); // 如果释放锁之后锁空闲,那么需要将锁持有者置为null
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free; // 返回锁释放后是否空闲
}

注意:

任何锁得释放都需要判断是否是在持有锁的情况下。如果不持有锁就释放,会抛出异常。对于写锁来说,判断是否持有锁很简单,只需要调用isHeldExclusively;而对于读锁来说,判断是否持有锁比较复杂,需要根据每个线程各自保存的持有读锁数来判断,即readHolds中保存的变量

尝试获取写锁

WriteLock使用tryLock来尝试获取写锁,如下:

public boolean tryLock( ) {
return sync.tryWriteLock();
}

tryLock内部实际调用的是Sync类定义并实现的tryWriteLock方法。该方法是一个final方法,不允许子类重写。其源码如下:

final boolean tryWriteLock() {
Thread current = Thread.currentThread();
int c = getState();
if (c != 0) {
int w = exclusiveCount(c);
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
}
if (!compareAndSetState(c, c + 1)) // 相比于tryAcquire方法,这里缺少对公平性判断(writerShouldBlock)
return false;
setExclusiveOwnerThread(current);
return true;
}

其实除了缺少对公平性判断方法writerShouldBlock的调用以外,和tryAcquire方法基本上是一样的,这里不再废话

Lock接口其他方法的实现

// 支持中断响应的lock方法,实际上调用的是AQS的acquireInterruptibly方法
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
} // 实际上调用的是AQS的方法tryAcquireNanos方法
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
} // 实际上调用的是Sync类实现的newCondition方法
public Condition newCondition() {
return sync.newCondition();
}

写锁是支持创建条件变量的,因为写锁是独占锁,而条件变量在await时会释放掉所有锁资源。写锁能够保证所有的锁资源都是本线程所持有,所以可以放心地去释放所有锁

而读锁不支持创建条件变量,因为读锁是共享锁,可能会有其他线程持有读锁。如果调用await,不仅会释放掉本线程持有的读锁,也会释放掉其他线程持有的读锁,这是不被允许的。因此读锁不支持条件变量

读锁

读锁是由内部类ReadLock实现的,其实现了Lock接口,获取锁、释放锁的逻辑都委托给了Sync类实例sync来执行。ReadLock的基本结构如下:

public static class ReadLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = -5992448646407690164L;
private final Sync sync; // 构造方法注入Sync类对象
protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
} // 实现了Lock接口的所有方法
}

获取读锁

ReadLock使用lock方法获取读锁,一次获取一个读锁。源码如下:

public void lock() {
sync.acquireShared(1);
}

lock方法内部实际调用的是AQS的acquireShared方法,源码如下:

public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}

该方法会调用Sync类实现的tryAcquireShared方法,源码如下:

protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1; // 如果写锁被获取,且并不是由本线程持有写锁,那么获取失败
int r = sharedCount(c);
if (!readerShouldBlock() && // 先进行公平性判断是否应该让步,这可能会导致重入读锁的线程获取失败
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) { // CAS失败可能也会导致本能获取成功的线程获取失败
// 如果此时读锁没有被获取,则该线程是第一个获取读锁的线程,记录相应信息
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
}
// 该线程不是首个获取读锁的线程,需要记录到readHolds中
else {
HoldCounter rh = cachedHoldCounter; // 通常当前获取读锁的线程就是最近获取到读锁的线程,所以直接用缓存
// 还是需要判断一下是不是最近获取到读锁的线程。如果不是,则调用get创建一个新的局部HoldCounter变量
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
// 之前最近获取读锁的线程如果释放完了读锁而导致其局部HoldCounter变量被remove了,这里重新获取就重新set
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1; // 如果公平性判断无需让步,且读锁数未饱和,且CAS竞争成功,则说明获取成功
}
return fullTryAcquireShared(current);
}

tryAcquireShared的返回值说明:

  • 负数:获取失败,线程会进入同步队列阻塞等待
  • 0:获取成功,但是后续以共享模式获取的线程都不可能获取成功(这里暂时用不上)
  • 正数:获取成功,且后续以共享模式获取的线程也可能获取成功

tryAcquireShared没有返回0的情况,只会返回正数或负数

前面“Sync类”中讲解过这些变量,这里再复习一遍:

  • firstReader、firstReaderHoldCount分别用于记录第一个获取到写锁的线程及其持有读锁的数量
  • cachedHoldCounter用于记录最后一个获取到写锁的线程持有读锁的数量
  • readHolds是一个线程局部变量(ThreadLocal变量),用于保存每个获得读锁的线程各自持有的读锁数量

tryAcquireShared的流程如下:

1、如果其他线程持有写锁,那么获取失败(返回-1)

2、否则,根据公平性判断是否应该阻塞。如果不用阻塞且读锁数量未饱和,则CAS请求读锁。如果CAS成功,获取成功(返回1),并记录相关信息

3、如果根据公平性判断应该阻塞,或者读锁数量饱和,或者CAS竞争失败,那么交给完整版本的获取方法fullTryAcquireShared去处理

其中步骤2如果发生了重入读(当前线程持有读锁的情况下,再次请求读锁),但根据公平性判断该线程需要阻塞等待,而导致重入读失败。按照正常逻辑,重入读不应该失败。不过,tryAcquireShared并没有处理这种情况,而是将其放到了fullTryAcquireShared中进行处理。此外,CAS竞争失败而导致获取读锁失败,也交给fullTryAcquireShared去处理(fullTryAcquireShared表示我好难)

fullTryAcquireShared方法是尝试获取读锁的完全版本,用于处理tryAcquireShared方法未处理的:

1、CAS竞争失败

2、因公平性判断应该阻塞而导致的重入读失败

这两种情况。其源码如下:

final int fullTryAcquireShared(Thread current) {
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()) {
// 如果当前线程就是firstReader,那么它一定是重入读,不让它失败,而是重新loop直到公平性判断不阻塞为止
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"); // 下面的逻辑基本上和tryAcquire中差不多,不过这里的CAS如果失败,会重新loop直到成功为止
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;
}
}
}

fullTryAcquireShared其实和tryAcquire存在很多的冗余之处,但这么做的目的主要是让tryAcquireShared变得更简单,不用处理复杂的CAS循环

fullTryAcquireShared主要是为了处理CAS失败和readerShouldBlock判true而导致的重入读失败,这两种情况在理论上都应该成功获取锁。fullTryAcquireShared的做法就是将这两种情况放在for循环中,一旦发生就重新循环,直到成功为止

tryAcquireShared和fullTryAcquireShared体现的读写锁特征:

  • 互斥关系:

    • 读锁和读锁之间是共享的:即使有其他线程持有了读锁,当前线程也能获取读锁
    • 读锁和写锁之间是互斥的。当有别的线程持有写锁,读锁不能获得:tryAcquireShared第4-6行,fullTryAcquireShared第5-7行都能体现这一特征
  • 可重入性:如果当前线程获取了读锁,那么它再次申请读锁一定能成功。这部分逻辑是由fullTryAcquireShared的for循环实现的
  • 支持锁降级:如果当前线程持有写锁,那么它申请读锁一定会成功。这部分逻辑见tryAcquireShared第5行,current和exclusiveOwnerThread是相等的,不会返回-1

释放读锁

ReadLock使用unlock方法释放读锁,如下:

public void unlock() {
sync.releaseShared(1);
}

unlock方法实际调用的是AQS的releaseShared方法,如下:

public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}

而该方法会调用Sync类实现的tryReleaseShared方法,源码如下:

protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) {
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(); // 如果释放读锁后不再持有锁,那么移除readHolds保存的线程局部HoldCounter变量
if (count <= 0)
throw unmatchedUnlockException(); // 抛出IllegalMonitorStateException异常
}
--rh.count;
}
// 循环CAS保证修改state成功
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
return nextc == 0; // 如果释放后锁空闲,那么返回true,否则返回false
}
}

如果返回true,说明锁是空闲的,releaseShared方法会进一步调用doReleaseShared方法,doReleaseShared方法会唤醒后继线程并确保传播(确保传播:保证被唤醒的线程可以执行唤醒其后续线程的逻辑

尝试释放读锁

ReadLock使用tryLock方法尝试释放读锁,源码如下:

public boolean tryLock() {
return sync.tryReadLock();
}

tryLock内部实际调用的是Sync类定义并实现的tryReadLock方法。该方法是一个final方法,不允许子类重写。其源码如下:

final boolean tryReadLock() {
Thread current = Thread.currentThread();
for (;;) {
int c = getState();
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return false;
int r = sharedCount(c);
if (r == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
if (compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
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 true;
}
}
}

其实除了缺少对公平性判断方法readerShouldBlock的调用以外,和tryAcquireShared方法基本上是一样的

Lock接口其他方法的实现

// 支持中断响应的lock方法,实际上调用的是AQS的acquireSharedInterruptibly方法
public void lockInterruptibly() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
} // 实际上调用的是AQS的方法tryAcquireSharedNanos方法
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
} // 读锁不支持创建条件变量
public Condition newCondition() {
throw new UnsupportedOperationException();
}

和写锁的区别在于,读锁不支持创建条件变量。如果调用newCondition方法,会直接抛出UnsupportedOperationException异常。不支持的原因在前面已经分析过,这里不再赘述

读写锁的公平性

公平读写锁

ReentrantReadWriteLock默认构造方法如下:

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

可见其默认创建的是非公平读写锁

公平读写锁依赖于Sync的子类FairSync实现,其源码如下:

static final class FairSync extends Sync {
private static final long serialVersionUID = -2274990926593161451L;
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}

writerShouldBlock

writerShouldBlock实际上调用的是AQS的hasQueuedPredecessors方法,该方法会检查是否有线程在同步队列中等待,源码如下:

public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
// 如果head等于tail,说明是空队列
// 如果队首的thread域不是当前线程,说明有别的线程先于当前线程等待获取锁
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}

writerShouldBlock只有在tryAcquire中被调用。如果当前线程请求写锁时发现已经有线程(读线程or写线程)在同步队列中等待,则让步

readerShouldBlock

readerShouldBlock和writerShouldBlock一样,都是调用AQS的hasQueuedPredecessors方法

readerShouldBlock只有在tryAcquireShared(fullTryAcquireShared)中被调用。如果当前线程请求读锁时发现已经有线程(读线程or写线程)在同步队列中等待,则让步

非公平读写锁

如果要创建非公平读写锁,需要使用有参构造函数,参数fair设置为true

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

非公平读写锁依赖于Sync的子类NonfairSync实现,其源码如下:

static final class NonfairSync extends Sync {
private static final long serialVersionUID = -8159625535654395037L;
final boolean writerShouldBlock() {
return false; // writers can always barge
}
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
}

writerShouldBlock

writerShouldBlock直接返回false

writerShouldBlock只有在tryAcquire中被调用,返回false表示在非公平模式下,不管是否有线程在同步队列中等待,请求写锁都不会让步,而是直接上去竞争

readerShouldBlock

readerShouldBlock实际调用的是AQS的apparentlyFirstQueuedIsExclusive方法。其源码如下:

final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}

如果同步队列为空,或队首线程是读线程(获取读锁而被阻塞),则返回false。如果同步队列队首线程是写线程(获取写锁而被阻塞),则返回true

readerShouldBlock只有在tryAcquireShared(fullTryAcquireShared)中被调用。如果当前线程请求读锁时发现同步队列队首线程是写线程,则让步。如果是读线程则跟它争夺锁资源

这么做的目的是为了防止写线程被“饿死”。因为如果一直有读线程前来请求锁,且读锁是有求必应,就会使得在同步队列中的写线程一直不能被唤醒。不过,apparentlyFirstQueuedIsExclusive只是一种启发式算法,并不能保证写线程一定不会被饿死。因为写线程有可能不在同步队列队首,而是排在其他读线程后面

读写锁的公平性总结

公平模式:

无论当前线程请求写锁还是读锁,只要发现此时还有别的线程在同步队列中等待(写锁or读锁),都一律选择让步

非公平模式:

  • 请求写锁时,当前线程会选择直接竞争,不会做丝毫的让步
  • 请求读锁时,如果发现同步队列队首线程在等待获取写锁,则会让步。不过这是一种启发式算法,因为写线程可能排在其他读线程后面

如果觉得作者写的还可以的话,可以鼓励一下,嘻嘻

全网最详细的ReentrantReadWriteLock源码剖析(万字长文)的更多相关文章

  1. 全网最详细的AbstractQueuedSynchronizer(AQS)源码剖析(一)AQS基础

    AbstractQueuedSynchronizer(以下简称AQS)的内容确实有点多,博主考虑再三,还是决定把它拆成三期.原因有三,一是放入同一篇博客势必影响阅读体验,而是为了表达对这个伟大基础并发 ...

  2. Redis源码剖析

    Redis源码剖析和注释(一)---链表结构 Redis源码剖析和注释(二)--- 简单动态字符串 Redis源码剖析和注释(三)--- Redis 字典结构 Redis源码剖析和注释(四)--- 跳 ...

  3. 全网最详细的AbstractQueuedSynchronizer(AQS)源码剖析(二)资源的获取和释放

    上期的<全网最详细的AbstractQueuedSynchronizer(AQS)源码剖析(一)AQS基础>中介绍了什么是AQS,以及AQS的基本结构.有了这些概念做铺垫之后,我们就可以正 ...

  4. Django对中间件的调用思想、csrf中间件详细介绍、Django settings源码剖析、Django的Auth模块

    目录 使用Django对中间件的调用思想完成自己的功能 功能要求 importlib模块介绍 功能的实现 csrf中间件详细介绍 跨站请求伪造 Django csrf中间件 form表单 ajax c ...

  5. (升级版)Spark从入门到精通(Scala编程、案例实战、高级特性、Spark内核源码剖析、Hadoop高端)

    本课程主要讲解目前大数据领域最热门.最火爆.最有前景的技术——Spark.在本课程中,会从浅入深,基于大量案例实战,深度剖析和讲解Spark,并且会包含完全从企业真实复杂业务需求中抽取出的案例实战.课 ...

  6. Apache Spark源码剖析

    Apache Spark源码剖析(全面系统介绍Spark源码,提供分析源码的实用技巧和合理的阅读顺序,充分了解Spark的设计思想和运行机理) 许鹏 著   ISBN 978-7-121-25420- ...

  7. 基于mybatis-generator-core 1.3.5项目的修订版以及源码剖析

    项目简单说明 mybatis-generator,是根据数据库表.字段反向生成实体类等代码文件.我在国庆时候,没事剖析了mybatis-generator-core源码,写了相当详细的中文注释,可以去 ...

  8. DICOM医学图形处理:storescp.exe与storescu.exe源码剖析,学习C-STORE请求(续)

    转载:http://blog.csdn.net/zssureqh/article/details/39237649 背景: 上一篇博文中,在对storescp工具源文件storescp.cc和DcmS ...

  9. DICOM医学图像处理:storescp.exe与storescu.exe源码剖析,学习C-STORE请求

    转载:http://blog.csdn.net/zssureqh/article/details/39213817 背景: 上一篇专栏博文中针对PACS终端(或设备终端,如CT设备)与RIS系统之间w ...

随机推荐

  1. 【做题记录】 [HEOI2013]SAO

    P4099 [HEOI2013]SAO 类型:树形 \(\text{DP}\) 这里主要补充一下 \(O(n^3)\) 的 \(\text{DP}\) 优化的过程,基础转移方程推导可以参考其他巨佬的博 ...

  2. 把二叉树打印成多行 牛客网 剑指Offer

    把二叉树打印成多行 牛客网 剑指Offer 题目描述 从上到下按层打印二叉树,同一层结点从左至右输出.每一层输出一行 # class TreeNode: # def __init__(self, x) ...

  3. linux 内核源代码情景分析——越界访问

    页式存储管理机制通过页面目录和页面表将每个线性地址转换成物理地址,当遇到下面几种情况就会使CPU产生一次缺页中断,从而执行预定的页面异常处理程序: ① 相应的页面目录或页表项为空,也就是该线性地址与物 ...

  4. (1)Zookeeper在linux环境中搭建集群

    1.简介 ZooKeeper是Apache软件基金会的一个软件项目,它为大型分布式计算提供开源的分布式配置服务.同步服务和命名注册.ZooKeeper的架构通过冗余服务实现高可用性.Zookeeper ...

  5. 倒谱Cepstrum本质的理解

    1.理解: 信号叠加时,不是都是线性关系(时域相互+ 频率相加):有的时候是两种信号成分相乘得到的,(时域卷积,频域相乘):比如很多齿轮啮合时振动信号调制现象,电机的轴向与径向的振动耦合时采集到的振动 ...

  6. Git - git push origin master 报错的解决方法

    亲测实用,转载保存,原文地址:https://blog.csdn.net/kangvcar/article/details/72773904 错误提示如下: [root@linux1 php]# gi ...

  7. Django 中间件 详细总结

    一.什么是中间件 中间件顾名思义,是介于request与response处理之间的一道处理过程,相对比较轻量级,并且在全局上改变django的输入与输出.因为改变的是全局,所以需要谨慎实用,用不好会影 ...

  8. Fiddler抓包工具简介:(四)Fiddler的基本使用

    Fiddler的使用 视图功能区域 会话的概念:一次请求和一次响应就是一个会话. fiddler主界面 下面挑几个快捷功能区中常用几项解释,其他功能自己尝试: 快捷功能区 1:给会话添加备注信息 2: ...

  9. TLFS 内存分配算法详解

    文章目录 1. DSA 背景介绍 1.1 mmheap 1.2 mmblk 2. TLFS 原理 2.1 存储结构 2.2 内存池初始化 2.3 free 2.4 malloc 参考资料 1. DSA ...

  10. JVM 核心参数

    JVM 内存相关的几个核心参数 参数部分看我笔记   https://note.youdao.com/s/Ch3awnVu JVM模板 1. ParNew + CMS 版 根据服务调整 -Xmx -X ...