概述

我们在介绍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方法,我们要先获取写锁。

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

源码分析

我们先来看下 ReentrantReadWriteLock 类的整体结构:

public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
private final ReentrantReadWriteLock.ReadLock readerLock;
/** Inner class providing writelock */
private final ReentrantReadWriteLock.WriteLock writerLock;
/** Performs all synchronization mechanics */
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。我们在上面的示例中使用到的 readLock() 和 writeLock() 方法就是返回这两个成员变量保存的锁实例。

我们在Sync类中可以看到下列代码:

        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; /** 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; }

可以看到主要是几个位移操作,通过上面的整体结构,我们知道了在读写锁内保存了读锁和写锁的两个实例。之前在ReentrantLock中,我们知道锁的状态是保存在Sync实例的state字段中的(继承自父类AQS),现在有了读写两把锁,然而可以看到还是只有一个Sync实例,那么一个Sync实例的state是如何同时保存两把锁的状态的呢?答案就是用了位分隔:

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

因此要获取独占锁当前的重入数量,就是 state & ((1 << 16) -1) (即 exclusiveCount 方法)

要获取共享锁当前的重入数量,就是 state >>> 16 (即 sharedCount 方法)

下面我们具体看写锁和读锁的实现。

写锁

看下WriteLock类中的lock和unlock方法:

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

可以看到就是调用的独占式同步状态的获取与释放,因此真实的实现就是Sync的 tryAcquire和 tryRelease。

写锁的获取

看下tryAcquire:

        protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c); //获取独占锁的重入数
if (c != 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;
}
//到这里了说明state为0,尝试直接cas。writerShouldBlock是为了实现公平或非公平策略的
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}

逻辑很简单,直接看注释就能理解。

写锁的释放

看下tryRelease:

        protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException(); //非独占模式直接抛异常
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null); //如果独占模式重入数为0了,说明独占模式被释放
setState(nextc); //不管独占模式是否被释放,更新独占重入数
return free;
}

逻辑很简单,直接看注释就能理解。

读锁

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

读锁的获取

        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);
//如果公平策略没有要求阻塞且重入数没有到达最大值,则直接尝试CAS更新state
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
//更新成功后会在firstReaderHoldCount中或readHolds(ThreadLocal类型的)的本线程副本中记录当前线程重入数(浅蓝色代码),这是为了实现jdk1.6中加入的getReadHoldCount()方法的,这个方法能获取当前线程重入共享锁的次数(state中记录的是多个线程的总重入次数),加入了这个方法让代码复杂了不少,但是其原理还是很简单的:如果当前只有一个线程的话,还不需要动用ThreadLocal,直接往firstReaderHoldCount这个成员变量里存重入数,当有第二个线程来的时候,就要动用ThreadLocal变量readHolds了,每个线程拥有自己的副本,用来保存自己的重入数。
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 1;
}
return fullTryAcquireShared(current); //用来处理CAS没成功的情况,逻辑和上面的逻辑是类似的,就是加了无限循环
}

下面这个方法就不用细说了,和上面的处理逻辑类似,加了无限循环用来处理CAS失败的情况。

        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()) {
// 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;
}
}
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
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;
}
}
}

读锁的释放

        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;
}
}

补充内容

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

在线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的时候,如果发现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)

在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)

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

综上:

一个线程要想同时持有写锁和读锁,必须先获取写锁再获取读锁;

写锁可以“降级”为读锁;

读锁不能“升级”为写锁。

总结

读写锁还是很实用的,因为一般场景下,数据的并发操作都是读多于写,在这种情况下,读写锁能够提供比排它锁更好的并发性。

在读写锁的实现方面,本来以为会比较复杂,结果看完源码的感受也是快刀切西瓜,看来AQS的设计真的很棒,在AQS的基础上构建的组件实现都很简单。

Java显式锁学习总结之五:ReentrantReadWriteLock源码分析的更多相关文章

  1. Java显式锁学习总结之六:Condition源码分析

    概述 先来回顾一下java中的等待/通知机制 我们有时会遇到这样的场景:线程A执行到某个点的时候,因为某个条件condition不满足,需要线程A暂停:等到线程B修改了条件condition,使con ...

  2. Java显式锁学习总结之二:使用AbstractQueuedSynchronizer构建同步组件

    Jdk1.5中包含了并发大神Doug Lea写的并发工具包java.util.concurrent,这个工具包中包含了显示锁和其他的实用同步组件.Doug Lea在构建锁和组件的时候,大多是以队列同步 ...

  3. Java显式锁学习总结之一:概论

    我们都知道在java中,当多个线程需要并发访问共享资源时需要使用同步,我们经常使用的同步方式就是synchronized关键字,事实上,在jdk1.5之前,只有synchronized一种同步方式.而 ...

  4. Java显式锁学习总结之四:ReentrantLock源码分析

    概述 ReentrantLock,即重入锁,是一个和synchronized关键字等价的,支持线程重入的互斥锁.只是在synchronized已有功能基础上添加了一些扩展功能. 除了支持可中断获取锁. ...

  5. Java并发编程笔记之读写锁 ReentrantReadWriteLock 源码分析

    我们知道在解决线程安全问题上使用 ReentrantLock 就可以,但是 ReentrantLock 是独占锁,同时只有一个线程可以获取该锁,而实际情况下会有写少读多的场景,显然 Reentrant ...

  6. Java显式锁学习总结之三:AbstractQueuedSynchronizer的实现原理

    概述 上一篇我们讲了AQS的使用,这一篇讲AQS的内部实现原理. 我们前面介绍了,AQS使用一个int变量state表示同步状态,使用一个隐式的FIFO同步队列(隐式队列就是并没有声明这样一个队列,只 ...

  7. Java显式锁

    Java 显式锁. 一.显式锁 什么是显式锁? 由自己手动获取锁,然后手动释放的锁. 有了 synchronized(内置锁) 为什么还要 Lock(显示锁)? 使用 synchronized 关键字 ...

  8. Java并发指南10:Java 读写锁 ReentrantReadWriteLock 源码分析

    Java 读写锁 ReentrantReadWriteLock 源码分析 转自:https://www.javadoop.com/post/reentrant-read-write-lock#toc5 ...

  9. 【Java并发编程】16、ReentrantReadWriteLock源码分析

    一.前言 在分析了锁框架的其他类之后,下面进入锁框架中最后一个类ReentrantReadWriteLock的分析,它表示可重入读写锁,ReentrantReadWriteLock中包含了两种锁,读锁 ...

随机推荐

  1. 【BZOJ2151】种树(贪心)

    [BZOJ2151]种树(贪心) 题面 BZOJ 题解 如果没有相邻不能选的限制,那么这就是一道傻逼题. 只需要用一个堆维护一下就好了. 现在加上了相邻点的限制,那么我们就对于当前位置加入一个撤销操作 ...

  2. SQL Server参数化SQL语句中的like和in查询的语法(C#)

    sql语句进行 like和in 参数化,按照正常的方式是无法实现的 我们一般的思维是: Like参数化查询:string sqlstmt = "select * from users whe ...

  3. YII2 model where 条件拼接

    熟悉Yii2的查询条件后,用Active Record查询数据非常方便. 以下我们介绍where()方法当中,条件的拼装方式. #某个值为null,会用IS NULL来生成语句: ['type' =& ...

  4. 【数学】【P5076】 Tweetuzki 爱整除

    Description 对于一个数 \(k\),找到任意一个 \(x\),满足 \(0~\leq~k~\leq~x~\leq~10^{18}\) 且对于任意一个 \(x\) 进制数,把该数字各数位上的 ...

  5. Codeforces 894.D Ralph And His Tour in Binary Country

    D. Ralph And His Tour in Binary Country time limit per test 2.5 seconds memory limit per test 512 me ...

  6. 「PLC」PLC基本编程

    PLC中无非就是三大量:开关量(数字量).模拟量.脉冲量.只在搞清楚三者之间的关系,你就能熟练的掌握PLC了. PLC编程算法(一) 1. 开关量也称逻辑量,指仅有两个取值,0或1.ON或OFF.它是 ...

  7. swift开发常用代码片段

    // 绑定事件 cell.privacySwitch.addTarget(self, action: #selector(RSMeSettingPrivacyViewController.switch ...

  8. matlab的rem()和mod()函数

    matlab的rem()和mod()函数 rem(x,y):求整除x/y的余数 mod(x,y):求模 rem(x,y)=x-y.*fix(x./y);  (fix()向0取整) mod(x,y)=x ...

  9. C++类四个默认函数&深复制&浅复制

    学习C++语言的同学都知道,C++中类是有默认的几个函数的,主要是有四个函数: 四个函数 默认构造函数:A(void),无参构造函数 拷贝(复制)构造函数:A(const A&a).用一个对象 ...

  10. 那些让 Web 开发者们深感意外的事情

    作为 Web 开发者,对自己的行业前景,人人都有自己的看法,然而,任何行业都有出人意料的地方.著名的 Web 开发设计博客 Nope.com 曾向他们的读者做了一个调查,请他们列举 Web 开发领域那 ...