1. 类继承关系


如图所示, ReadWriteLock 是一个接口,内部由两个Lock 接口组成。

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

ReentrantReadWriteLock 实现了该接口,使用方式如下:

ReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock rLock = rwLock. readLock();
rLock.lock();
rLock.unlock();
Lock wLock = rwLock.writeLock();
wLock.lock();
wLock.unlock();

也就是说,当使用ReadWriteLock 的时候, 并不是直接使用,而是获得其内部的读锁和写锁,然后分别调用lock/unlock 。

2. 读写锁实现的基本原理

从表面来看, ReadLock 和WriteLock 是两把锁,实际上它只是同一把锁的两个视图而己。什么叫两个视图呢?可以理解为是一把锁,线程分成两类:读线程和写线程。读线程和读线程之间不互斥(可以同时拿到这把锁〉,读线程和写线程互斥,写线程和写线程也互斥。
从下面的构造函数也可以看出, readerLock 和writerLock 实际共用同一个sync 对象。sync对象同互斥锁一样,分为非公平和公平两种策略,井继承自AQS 。

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

同互斥锁一样,读写锁也是用state 变量来表示锁状态的。只是state 变量在这里的含义和互斥锁完全不同。在内部类Sync 中,对state 变量进行了重新定义,如下所示。

abstract static class Sync extends AbstractQueuedSynchronizer {
/** 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; }
}

也就是把state 变量拆成两半,低16 位,用来记录写锁。但同一时间既然只能有一个线程写,为什么还需要16 位呢?这是因为一个写线程可能多次重入。例如,低16 位的值等于5 ,表示一个写线程重入了5 次。
高16 位,用来“读”锁。例如,高16 位的值等于5 ,可以表示5 个读线程都拿到了该锁;也可以表示一个读线程重入了5 次。
这个地方的设计很巧妙, 为什么要把一个int 类型变量拆成两半,而不是用两个int 型变量分别表示读锁和写锁的状态呢?这是因为无法用一次CAS 同时操作两个int 变量,所以用了一个int 型的高16 位和低16 位分别表示读锁和写锁的状态。
当state = 0 时,说明既没有线程持有读锁,也没有线程持有写锁:当state != 0 时,要么有线程持有读锁,要么有线程持有写锁,两者不能同时成立,因为读和写互斥。这时再进一步通过sharedCount(state)和exclusiveCount(state)判断到底是读线程还是写线程持有了该锁。

3. AQS 的两对模板方法

下面介绍在ReentrantReadWriteLock 的两个内部类ReadLock 和WriteLock中,是如何使用state 变量的。

public static class ReadLock implements Lock, java.io.Serializable {public void lock() {sync.acquireShared(1);}public void unlock() {sync.releaseShared(1);}...
}public static class WriteLock implements Lock, java.io.Serializable {public void lock() {sync.acquire(1);}public void unlock() {sync.release(1);}...
}

acquire / release 、acquireShared/releaseShared 是AQS 里面的两对模板方法。互斥锁和读写锁的写暂且都是基于acquire /release 模板方法来实现盹读写锁的读暂且是基于acquireShared/ releaseShared这对模板方法来实现的。这两对模板方法的代码如下:

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizerimplements java.io.Serializable {public final void acquire(int arg) {if (!tryAcquire(arg) &&             // tryAcquire被各种Sync子类实现acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();}public final boolean release(int arg) {if (tryRelease(arg)) {                // tryRelease被各种Sync子类实现Node h = head;if (h != null && h.waitStatus != 0)unparkSuccessor(h);return true;}return false;}public final void acquireShared(int arg) {if (tryAcquireShared(arg) < 0)       // tryAcquireShared被各种Sync子类实现doAcquireShared(arg);}public final boolean releaseShared(int arg) {if (tryReleaseShared(arg)) {       // tryReleaseShared被各种Sync子类实现doReleaseShared();return true;}return false;}
}

将读/写、公平/非公平进行排列组合,就有4 种组合。如图下图所示,上面的两个函数都是在Sync 中实现的。Sync 中的两个函数又是模板方法,在NonfairSync 和FairSync 中分别有实现。最终的对应关系如下:

(1)读锁的公平实现: Sync.tryAccquireShared() + FairSync 中的两个覆写的子函数。
(2)读锁的非公平实现: Sync.tryAccquireShared() + NonfairSync 中的两个覆写的子函数。
(3)写锁的公平实现: Sync.tryAccquire() + FairSync 中的两个覆写的子函数。
(4)写锁的非公平实现: Sync.tryAccquire() + NonfairSync 中的两个覆写的子函数。

static final class NonfairSync extends Sync {private static final long serialVersionUID = -8159625535654395037L;final boolean writerShouldBlock() {                     // 写线程抢锁的时候是否应该阻塞,return false;                                      // 写线程在抢锁之前永远不被阻塞,是非公平的}final boolean readerShouldBlock() {                      // 读线程抢锁的时候是否应该阻塞,return apparentlyFirstQueuedIsExclusive();         // 读线程抢锁的时侃当队列中第1个元素是写线程的时候,要阻塞}}static final class FairSync extends Sync {final boolean writerShouldBlock() {           // 写线程抢锁的时候是否应该阻塞return hasQueuedPredecessors();            // 写线程在抢锁之前,如果队列里有其他线程在排队,就要阻塞,所以是公平的}final boolean readerShouldBlock() {      // 读线程抢锁的时候是否应该阻塞return hasQueuedPredecessors();        // 读线程在抢锁之前,如果队列里有其他线程在排队,就要阻塞,所以是公平的}}

上面的代码介绍了ReentrantReadWriteLock 里面的NonfairSync 和FairSync 的实现过程,对应了上面的四种实现策略,下面分别解释。
对于公平,比较容易理解,不论是读锁,还是写锁,只要队列中有其他线程在排队(排队等读锁,或者排队等写锁),就不能直接去抢锁,要排在队列尾部。
对于非公平,读锁和写锁的实现策略略有差异。先说写锁,写线程能抢锁,前提是state= 0,只有在没有其他线程持有读锁或写锁的情况下,它才有机会去抢锁。或者state != 0 ,但那个持有写锁的线程是它自己,再次重入。写线程是非公平的,就是不管三七二十一就去抢,即一直
返回false 。
但对于读线程,能否也不管三七二十二,上来就去抢呢?不行!因为读线程和读线程是不互斥的,假设当前线程被读线程持有,然后其他读线程还非公平地一直去抢,可能导致写线程永远拿不到锁,所以对于读线程的非公平,要做一些“约束”。当发现队列的第l 个元素是写线程的时候,读线程也要阻塞一下, 不能“肆无忌惮”地直接去抢。
明白策略后,下面具体介绍四种实现方面的差异。

4. WriteLock公平与非公平实现

写锁是排他锁,实现策略类似于互斥锁,重写了tryAcquire/tryRelease 方法。

4.1 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)if (w == 0 || current != getExclusiveOwnerThread())return false;if (w + exclusiveCount(acquires) > MAX_COUNT)throw new Error("Maximum lock count exceeded");// Reentrant acquiresetState(c + acquires);return true;}if (writerShouldBlock() ||!compareAndSetState(c, c + acquires))return false;setExclusiveOwnerThread(current);return true;}

把上面的代码拆开进行分析,如下:

(1) if(c !=0) and w== 0 ,说明当前一定是读线程拿着锁,写锁一定拿不到,返回false ,
(2) if (c != 0) and w != 0 ,说明当前一定是写线程拿着锁,执行current != getExclusiveOwnerThread()的判断,发现ownerThread
不是自己,返回false 。
(3) c != o, w != 0 ,且current = getExclusiveOwnerThread(),才会走到if (w + exclusiveCount(acquires) > MAX_COUNT) 。
判断重入次数,重入次数超过最大值,抛出异常。
因为是用state 的低16位保存写锁重入次数的,所以MAXCOUNT 是2的16次方 。如果超出这个值, 会写到读锁的高16 位上。为了避免
这种情形,这里做了一个检测。当然, 一般不可能重入这么多次。
(4) if(c = 0),说明当前既没有读线程,也没有写线程持有该锁。可以通过CAS 操作开抢了。

if (writerShouldBlock ()  || !compareAndSetState(c, c + acquires))

抢成功后,调用setExclusiveOwnerThread(current),把ownerThread 设成自己。
公平实现和非公平实现几乎一模一样,只是writerShouldBlock()分别被FairSync 和NonfairSync 实现,在上一节己讲。

4.2 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);}setState(nextc);          //  因为写锁是排他的,在当前线程持有写锁的时候,其他线程既不会持有写锁,// 不会持有读锁。所以,这里对state 值的调减不需要CAS 操作,直接减1即可return free;}

5. ReadLock公平与非公平实现

读锁是共享锁,重写了tryAcquireShared/tryReleaseShared方法,其实现策略和排他锁有很大的差异。

5.1 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 拿读锁,高16 位加1if (r == 0) {        // 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);      // 上面拿读锁失败,进入这个函数不断自旋拿读锁}

(1)

if (exclusiveCount (c) != 0 && getExclusiveOwnerThread () != current)return - 1 ;

低16 位不等于0 ,说明有写线程持有锁,并且只有当ownerThread != 自己时,才返回-1 。这里面有一个潜台词:如果current = ownerThread ,则这段代码不会返回。这是因为- 个写线程可以再次去拿读锁!也就是说, 一个线程在持有了WriteLock 后,再去调用ReadLock.lock 也是可以的。
(2) 上面的compareAndSetState(c, c + SHARED_ UNIT), 其实是把state 的高16 位加1 (读锁的状态),但因为是在高16 位,必须把1左移16 位再加1 。
(3) firstReader, cachedHoldConunter 之类的变量,只是一些统计变量, 在ReentrantReadWriteLock对外的一些查询函数中会用到,例如,查询持有读锁的线程列表,但对整个读写互斥机制没有影响,此处不再展开解释。

5.2 tryReleaseShared(…)实现分析

protected final boolean tryReleaseShared(int unused) {Thread current = Thread.currentThread();...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;}}

因为读锁是共享锁,多个线程会同时持有读锁,所以对读锁的释放不能直接减1 ,而是需要通过一个for 循环+ CAS 操作不断重试。这是tryReleaseShared 和tryRelease 的根本差异所在。

读写锁 -- ReentrantReadWriteLock相关推荐

  1. Java Review - 并发编程_读写锁ReentrantReadWriteLock的原理源码剖析

    文章目录 ReentrantLock VS ReentrantReadWriteLock 类图结构 非公平的读写锁实现 写锁的获取与释放 void lock() void lockInterrupti ...

  2. 并发编程-19AQS同步组件之重入锁ReentrantLock、 读写锁ReentrantReadWriteLock、Condition

    文章目录 J.U.C脑图 ReentrantLock概述 ReentrantLock 常用方法 synchronized 和 ReentrantLock的比较 ReentrantLock示例 读写锁R ...

  3. java 可重入读写锁 ReentrantReadWriteLock 详解

    详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt206 读写锁 ReadWriteLock读写锁维护了一对相关的锁,一个用于只 ...

  4. 深入分析实战可重入读写锁ReentrantReadWriteLock

    文章目录 前言 加锁规则 同步原理 源码解析 实战演示 前言 前面我们学习了可重入锁ReentrantLock,可重入锁是一个排他锁,只要不是当前线程访问加锁资源都不能够进入,只能等待锁的释放.当然, ...

  5. 深入理解读写锁ReentrantReadWriteLock

    深入理解读写锁ReentrantReadWriteLock 前言 业务开发中我们可能涉及到读写操作. 面对写和读,对于数据同步,在使用Lock锁和 synchronized关键字同步数据时候,对于读读 ...

  6. 读写锁ReentrantReadWriteLock:读读共享,读写互斥,写写互斥

    JDK1.5之后,提供了读写锁ReentrantReadWriteLock,读写锁维护了一对锁,一个读锁,一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升.在读多写少的情况下,读写 ...

  7. Oracle java官网关于可重入读写锁ReentrantReadWriteLock的解析

    Oracle java官网关于可重入读写锁ReentrantReadWriteLock的解析 1.[原文链接](https://docs.oracle.com/javase/8/docs/api/ja ...

  8. jdk读写锁ReentrantReadWriteLock

    public ReentrantReadWriteLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();reader ...

  9. 读写锁ReentrantReadWriteLock源码分析

    文章目录 读写锁的介绍 写锁详解 写锁的获取 写锁的释放 读锁详解 读锁的获取 读锁的释放 锁降级 读写锁的介绍 在并发场景中用于解决线程安全的问题,我们几乎会高频率的使用到独占式锁,通常使用java ...

最新文章

  1. android 让item满屏,Android的全屏活动?
  2. qml mousearea 点击其他地方_Qml 快速使用
  3. oracle外网监听端口,oracle 11g 修改默认监听端口1521
  4. ROBOTS.TXT屏蔽笔记、代码、示例大全
  5. 53pagecontext对象
  6. hive java导入CVS
  7. 哪些是MySQL用于放置一些头文件的目录_在古希腊的知识体系中,两大部分是( )。...
  8. (七)Netty与零拷贝
  9. 使用 Github 作为专用 Nuget 包服务器并共享您的包
  10. 【Redis数据库】命令学习笔记——发布订阅、事务、脚本、连接等命令汇总
  11. 利用百度地图API,在浏览器中找到自己的位置
  12. 简述python调试程序_简单调试 Python 程序
  13. PLC基本指令系统优势
  14. oracle补丁安装
  15. TSP、MTSP问题遗传算法详细解读及python实现
  16. ASCII字符绘图网站推荐及使用Python绘制ASCII字符画
  17. labview与android,LabVIEW与Android手机的无线视频实时传输
  18. DSP28335 eCAP 测频
  19. [Mark]The problems solutions of vmware vsphere
  20. XML是什么鬼?全称叫啥勒?

热门文章

  1. 足球数据API接口 - 【足球赛事分析数据】API调用示例代码
  2. CAD文件低版本怎么转换高版本
  3. Creo4.0管道管线设计视频教程
  4. php 关键词生成,全自动无限生成关键词页面(黑帽SEO优化终极方法)
  5. 弘辽科技:同为女强人,人生道路却大不相同。
  6. led灯串怎么摆造型_一种LED轮廓造型灯的制作方法
  7. LitJson在Unity中的使用
  8. IOS FMDB 使用
  9. 机房服务器安装操作系统
  10. Kaggle word2vec NLP 教程 第三部分:词向量的更多乐趣