ReadWriteLock 和 ReentrantReadWrite介绍

ReadWriteLock,顾名思义,是读写锁。
它维护了一对相关的锁 — — “读取锁”和“写入锁”,一个用于读取操作,另一个用于写入操作。

“读取锁”用于只读操作,它是“共享锁”,能同时被多个线程获取。

“写入锁”用于写入操作,它是“独占锁”,写入锁只能被一个线程锁获取。

这是它的函数列表:

// 返回用于读取操作的锁。
Lock readLock()
// 返回用于写入操作的锁。
Lock writeLock()

而对于ReentrantReadWriteLock,是否觉得ReentrantReadWriteLock会实现Lock接口呢?

答案是否定的,ReentrantReadWriterLock采用组合的方式,采用两个内部类实现Lock接口,分别是ReadLock,WriterLock类。

与ReentrantLock一样,ReentrantReadWriterLock同样使用自己的内部类Sync(继承AbstractQueuedSynchronizer)实现CLH算法,同时让sysn可以指向它的子类FairSync和NonFaireSync。

这是它的uml图:

ReentrantReadWrite的属性

为了更好地对读写锁机制的了解和下面源码的解读,先介绍一下它的组件Sync的成员:

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

首先ReentrantReadWriterLock使用一个32位的int类型来表示锁被占用的线程数(ReentrantLock中的state),用所以,采取的办法是,高16位用来表示读锁占有的线程数量,用低16位表示写锁被同一个线程申请的次数。

  • SHARED_SHIFT,表示读锁占用的位数,常量16。
  • SHARED_UNIT, 增加一个读锁,按照上述设计,就相当于增加 SHARED_UNIT;
  • MAX_COUNT ,表示申请读锁最大的线程数量,为65535
  • EXCLUSIVE_MASK :表示计算写锁的具体值时,该值为 15个1,用 getState & EXCLUSIVE_MASK算出写锁的线程数,大于1表示重入。

     static int sharedCount(int c)    { return c >>> SHARED_SHIFT; } static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

举例说明,比如,现在当前,申请读锁的线程数为13个,写锁一个,那state怎么表示?上文说过,用一个32位的int类型的高16位表示读锁线程数,13的二进制为 1101,那state的二进制表示为
00000000 00001101 00000000 00000001,十进制数为851969。

sharedCount方法获取的是共享锁,也就是读锁,只需要将state 无符号向左移位16位置,得出00000000 00001101,就出13。

exclusiveCount根据851969要算成低16位置,只需要用该00000000 00001101 00000000 00000001 & 111111111111111(15位),就可以得出00000001。

这几个参数看懂了,下面来几个有意思的成员:

  private transient ThreadLocalHoldCounter readHolds;  private transient HoldCounter cachedHoldCounter; private transient Thread firstReader = null;  private transient int firstReaderHoldCount;  

ThreadLocalHoldCounter和HoldCounter是什么?

        /*** A counter for per-thread read hold counts.* Maintained as a ThreadLocal; cached in cachedHoldCounter*/static final class HoldCounter {int count = 0;// Use id, not reference, to avoid garbage retentionfinal long tid = getThreadId(Thread.currentThread());}/*** ThreadLocal subclass. Easiest to explicitly define for sake* of deserialization mechanics.*/static final class ThreadLocalHoldCounterextends ThreadLocal<HoldCounter> {public HoldCounter initialValue() {return new HoldCounter();}}

HoldCounter存着读锁的数量。而ThreadLocalHoldCounter继承了线程本地类ThreadLocal,持有一个HoldCounter对象,也就是说ThreadLocalHoldCounter持有当前线程的读锁的数量。

firstReader与firstReadHoldCount保存第一个获取读锁的线程,也就是readHolds中并不会保存第一个获取读锁的线程;cachedHoldCounter 缓存的是最后一个获取线程的HolderCount信息,该变量主要是在如果当前线程多次获取读锁时,减少从readHolds中获取HoldCounter的次数。为什么要把第一个读的线程的信息和最后一个的信息单独拿出来呢,这是一种优化方法,缓存使用。看到后面就清楚了。

获取共享锁的过程

获取共享锁的思想(即lock函数的步骤),是先通过tryAcquireShared()尝试获取共享锁。尝试成功的话,则直接返回;尝试失败的话,则通过doAcquireShared()不断的循环并尝试获取锁,若有需要,则阻塞等待。doAcquireShared()在循环中每次尝试获取锁时,都是通过tryAcquireShared()来进行尝试的。下面看看“获取共享锁”的详细流程。

1.lock方法

lock()在ReadLock中,切记共享锁在ReadLock中,WriteLock持有的是独占锁。

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

2.acquireShared方法

Sync继承于AQS,acquireShared()定义在AQS中。源码如下:

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

与此类似地,我们可以对比一下同在AQS的独占锁的acquire()

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

acquireShared()首先会通过tryAcquireShared()来尝试获取锁。

  • 尝试成功的话,则不再做任何动作(因为已经成功获取到锁了)。
  • 尝试失败的话,则通过doAcquireShared()来获取锁。doAcquireShared()会获取到锁了才返回。

3. tryAcquire

tryAcquire定义在AQS中,由Sync类重写了:

protected final int tryAcquireShared(int unused) {Thread current = Thread.currentThread();// 获取“锁”的状态int c = getState();// 如果“锁”是“写锁”,并且获取锁的线程不是current线程;则返回-1。if (exclusiveCount(c) != 0 &&getExclusiveOwnerThread() != current)return -1;// 获取“读取锁”的共享计数int r = sharedCount(c);// 如果“不需要阻塞等待”,并且“读取锁”的共享计数小于MAX_COUNT;// 则通过CAS函数更新“锁的状态”,将“读取锁”的共享计数+1。if (!readerShouldBlock() &&r < MAX_COUNT &&compareAndSetState(c, c + SHARED_UNIT)) {// 第1次获取“读取锁”。if (r == 0) { firstReader = current;firstReaderHoldCount = 1;// 如果想要获取锁的线程(current)是第1个获取锁(firstReader)的线程} else if (firstReader == current) { firstReaderHoldCount++;} else {// 获得最后一个获取读线程的信息HoldCounter rh = cachedHoldCounter;//如果不存在或者不是当前线程,就从readHolds获得当前线程信息     //加一并添加到缓存cachedHoldCounter中if (rh == null || rh.tid != current.getId())cachedHoldCounter = rh = readHolds.get();else if (rh.count == 0)readHolds.set(rh);// 将该线程获取“读取锁”的次数+1。rh.count++;}return 1;}return fullTryAcquireShared(current);
}

说明:tryAcquireShared()的作用是尝试获取“共享锁”。
如果在尝试获取锁时,“不需要阻塞等待”并且“读取锁的共享计数小于MAX_COUNT”,则直接通过CAS函数更新“读取锁的共享计数”,然后更新下面的Sync类的成员:

  private transient ThreadLocalHoldCounter readHolds;  private transient HoldCounter cachedHoldCounter; private transient Thread firstReader = null;  private transient int firstReaderHoldCount;  

从代码中,我们可以清楚地看到下面的三个都是在为readHolds服务,如果没有这三个,那么获取当前线程的读锁数量的操作只能是readHolds.get(),所以通过这三个成员相当于优化,缓存。

获取不到锁,就要用fullTryAcquireShared获得。

4. fullTryAcquireShared()

final int fullTryAcquireShared(Thread current) {HoldCounter rh = null;for (;;) {// 获取“锁”的状态int c = getState();// 如果“锁”是“互斥锁”,并且获取锁的线程不是current线程;则返回-1。if (exclusiveCount(c) != 0) {if (getExclusiveOwnerThread() != current)return -1;// 如果“需要阻塞等待”。// (01) 当“需要阻塞等待”的线程是第1个获取锁的线程的话,则继续往下执行。// (02) 当“需要阻塞等待”的线程获取锁的次数=0时,则返回-1。} else if (readerShouldBlock()) {// 如果想要获取锁的线程(current)是第1个获取锁(firstReader)的线程if (firstReader == current) {} else {if (rh == null) {rh = cachedHoldCounter;if (rh == null || rh.tid != current.getId()) {rh = readHolds.get();if (rh.count == 0)readHolds.remove();}}// 如果当前线程获取锁的计数=0,则返回-1。if (rh.count == 0)return -1;}}// 如果“不需要阻塞等待”,则获取“读取锁”的共享统计数;// 如果共享统计数超过MAX_COUNT,则抛出异常。if (sharedCount(c) == MAX_COUNT)throw new Error("Maximum lock count exceeded");// 将线程获取“读取锁”的次数+1。if (compareAndSetState(c, c + SHARED_UNIT)) {// 如果是第1次获取“读取锁”,则更新firstReader和firstReaderHoldCount。if (sharedCount(c) == 0) {firstReader = current;firstReaderHoldCount = 1;// 如果想要获取锁的线程(current)是第1个获取锁(firstReader)的线程,// 则将firstReaderHoldCount+1。} else if (firstReader == current) {firstReaderHoldCount++;} else {if (rh == null)rh = cachedHoldCounter;if (rh == null || rh.tid != current.getId())rh = readHolds.get();else if (rh.count == 0)readHolds.set(rh);// 更新线程的获取“读取锁”的共享计数rh.count++;cachedHoldCounter = rh; // cache for release}return 1;}}
}

说明:fullTryAcquireShared()会根据“是否需要阻塞等待”,“读取锁的共享计数是否超过限制”等等进行处理。如果不需要阻塞等待,并且锁的共享计数没有超过限制,则通过CAS尝试获取锁,并返回1。

doAcquireShared方法

private void doAcquireShared(int arg) {// addWaiter(Node.SHARED)的作用是,创建“当前线程”对应的节点,并将该线程添加到CLH队列中。final Node node = addWaiter(Node.SHARED);boolean failed = true;try {boolean interrupted = false;for (;;) {// 获取“node”的前一节点final Node p = node.predecessor();// 如果“当前线程”是CLH队列的表头,则尝试获取共享锁。if (p == head) {int r = tryAcquireShared(arg);if (r >= 0) {setHeadAndPropagate(node, r);p.next = null; // help GCif (interrupted)selfInterrupt();failed = false;return;}}// 如果“当前线程”不是CLH队列的表头,则通过shouldParkAfterFailedAcquire()判断是否需要等待,// 需要的话,则通过parkAndCheckInterrupt()进行阻塞等待。若阻塞等待过程中,线程被中断过,则设置interrupted为true。if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}
}

doAcquireShared()的作用是获取共享锁。
它会首先创建线程对应的CLH队列的节点,然后将该节点添加到CLH队列中。CLH队列是管理获取锁的等待线程的队列。
如果“当前线程”是CLH队列的表头,则尝试获取共享锁;否则,则需要通过shouldParkAfterFailedAcquire()判断是否阻塞等待,需要的话,则通过parkAndCheckInterrupt()进行阻塞等待。
doAcquireShared()会通过for循环,不断的进行上面的操作;目的就是获取共享锁。

需要注意的是:doAcquireShared()在每一次尝试获取锁时,是通过tryAcquireShared()来执行的!

释放共享锁的过程

释放共享锁的思想,是先通过tryReleaseShared()尝试释放共享锁。尝试成功的话,则通过doReleaseShared()唤醒“其他等待获取共享锁的线程”,并返回true;否则的话,返回flase。

1. unlock()

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

2. releaseShared()

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

说明:releaseShared()的目的是让当前线程释放它所持有的共享锁。
它首先会通过tryReleaseShared()去尝试释放共享锁。尝试成功,则直接返回;尝试失败,则通过doReleaseShared()去释放共享锁。这个过程实际上跟lock的操作相似。

3. tryReleaseShared()

protected final boolean tryReleaseShared(int unused) {// 获取当前线程,即释放共享锁的线程。Thread current = Thread.currentThread();// 如果想要释放锁的线程(current)是第1个获取锁(firstReader)的线程,// 并且“第1个获取锁的线程获取锁的次数”=1,则设置firstReader为null;// 否则,将“第1个获取锁的线程的获取次数”-1。if (firstReader == current) {// assert firstReaderHoldCount > 0;if (firstReaderHoldCount == 1)firstReader = null;elsefirstReaderHoldCount--;// 获取rh对象,并更新“当前线程获取锁的信息”。} else {HoldCounter rh = cachedHoldCounter;if (rh == null || rh.tid != current.getId())rh = readHolds.get();int count = rh.count;if (count <= 1) {readHolds.remove();if (count <= 0)throw unmatchedUnlockException();}--rh.count;}for (;;) {// 获取锁的状态int c = getState();// 将锁的获取次数-1。int nextc = c - SHARED_UNIT;// 通过CAS更新锁的状态。if (compareAndSetState(c, nextc))return nextc == 0;}
}

4. doReleaseShared()

private void doReleaseShared() {for (;;) {// 获取CLH队列的头节点Node h = head;// 如果头节点不为null,并且头节点不等于tail节点。if (h != null && h != tail) {// 获取头节点对应的线程的状态int ws = h.waitStatus;// 如果头节点对应的线程是SIGNAL状态,则意味着“头节点的下一个节点所对应的线程”需要被unpark唤醒。if (ws == Node.SIGNAL) {// 设置“头节点对应的线程状态”为空状态。失败的话,则继续循环。if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))continue;// 唤醒“头节点的下一个节点所对应的线程”。unparkSuccessor(h);}// 如果头节点对应的线程是空状态,则设置“文件点对应的线程所拥有的共享锁”为其它线程获取锁的空状态。else if (ws == 0 &&!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))continue;                // loop on failed CAS}// 如果头节点发生变化,则继续循环。否则,退出循环。if (h == head)                   // loop if head changedbreak;}
}

doReleaseShared()会释放“共享锁”。它会从前往后的遍历CLH队列,依次“唤醒”然后“执行”队列中每个节点对应的线程;最终的目的是让这些线程释放它们所持有的锁。

公平共享锁和非公平共享锁

与互斥锁一样ReenTrantLock一样,共享锁ReadLock也分为公平锁和非公平锁。

我们回顾一下ReenTrantLock,公平锁和非公平锁的区别在于lock()和tryAcquire()方法允许了插队。

我们来看一下ReenTrantReadWriteLock中的FairSync和NonSync的区别:
![](/Users/yj/Desktop/屏幕快照 2018-01-13 下午12.00.53.png)

很容易看出来,ReadLock公平锁和非公平锁的区别在于readerShouldBlock方法:

公平锁:

final boolean readerShouldBlock() {return hasQueuedPredecessors();
}

在公平共享锁中,如果在当前线程的前面有其他线程在等待获取共享锁,则返回true;否则,返回false。

非公平锁:

final boolean readerShouldBlock() {return apparentlyFirstQueuedIsExclusive();
}

使用ReentrantReadWriteLock的例子:

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock; public class ReadWriteLockTest1 { public static void main(String[] args) { // 创建账户MyCount myCount = new MyCount("4238920615242830", 10000); // 创建用户,并指定账户User user = new User("Tommy", myCount); // 分别启动3个“读取账户金钱”的线程 和 3个“设置账户金钱”的线程for (int i=0; i<3; i++) {user.getCash();user.setCash((i+1)*1000);}}
} class User {private String name;            //用户名 private MyCount myCount;        //所要操作的账户 private ReadWriteLock myLock;   //执行操作所需的锁对象 User(String name, MyCount myCount) {this.name = name; this.myCount = myCount; this.myLock = new ReentrantReadWriteLock();}public void getCash() {new Thread() {public void run() {myLock.readLock().lock(); try {System.out.println(Thread.currentThread().getName() +" getCash start"); myCount.getCash();Thread.sleep(1);System.out.println(Thread.currentThread().getName() +" getCash end"); } catch (InterruptedException e) {} finally {myLock.readLock().unlock(); }}}.start();}public void setCash(final int cash) {new Thread() {public void run() {myLock.writeLock().lock(); try {System.out.println(Thread.currentThread().getName() +" setCash start"); myCount.setCash(cash);Thread.sleep(1);System.out.println(Thread.currentThread().getName() +" setCash end"); } catch (InterruptedException e) {} finally {myLock.writeLock().unlock(); }}}.start();}
}class MyCount {private String id;         //账号 private int    cash;       //账户余额 MyCount(String id, int cash) { this.id = id; this.cash = cash; } public String getId() { return id; } public void setId(String id) { this.id = id; } public int getCash() { System.out.println(Thread.currentThread().getName() +" getCash cash="+ cash); return cash; } public void setCash(int cash) { System.out.println(Thread.currentThread().getName() +" setCash cash="+ cash); this.cash = cash; }
}

运行结果:

Thread-0 getCash start
Thread-2 getCash start
Thread-0 getCash cash=10000
Thread-2 getCash cash=10000
Thread-0 getCash end
Thread-2 getCash end
Thread-1 setCash start
Thread-1 setCash cash=1000
Thread-1 setCash end
Thread-3 setCash start
Thread-3 setCash cash=2000
Thread-3 setCash end
Thread-4 getCash start
Thread-4 getCash cash=2000
Thread-4 getCash end
Thread-5 setCash start
Thread-5 setCash cash=3000
Thread-5 setCash end

说明:

(01) 观察Thread0和Thread-2的运行结果,我们发现,Thread-0启动并获取到“读取锁”,在它还没运行完毕的时候,Thread-2也启动了并且也成功获取到“读取锁”。
因此,“读取锁”支持被多个线程同时获取。

(02) 观察Thread-1,Thread-3,Thread-5这三个“写入锁”的线程。“写入锁”不支持被多个线程同时获取。

(03) 观察Thread-3,Thread-4,Thread-5这三个“写入锁”的线程,读写互斥,只要“写入锁”被某线程获取,则该线程运行完毕了,才释放该锁。

简单来说: “读-读”不互斥,”读-写”互斥,”写-写”互斥

可能令人比较好奇的是,为什么readLock,writeLock两个不同的对象,为什么他们之间是互斥的呢?

原因很简单:
因为readLock,writeLock的构造方法都要传入ReentrantReadWriteLock中的Sysn锁,Sysn锁也是ReentrantReadWriteLock的组件,是唯一的。

JUC锁-ReentrantReadWrite(五)相关推荐

  1. JUC锁框架AbstractQueuedSynchronizer详细分析

    转载自:https://www.jianshu.com/p/0da2939391cf AQS是JUC锁框架中最重要的类,通过它来实现独占锁和共享锁的.本章是对AbstractQueuedSynchro ...

  2. JUC锁框架_AbstractQueuedSynchronizer详细分析

    AQS是JUC锁框架中最重要的类,通过它来实现独占锁和共享锁的.本章是对AbstractQueuedSynchronizer源码的完全解析,分为四个部分介绍: CLH队列即同步队列:储存着所有等待锁的 ...

  3. Java多线程系列--“JUC锁”05之 非公平锁

    转载自:http://www.cnblogs.com/skywang12345/p/3496651.html点击打开链接 概要 前面两章分析了"公平锁的获取和释放机制",这一章开始 ...

  4. Java多线程系列---“JUC锁”01之 框架

    本章,我们介绍锁的架构:后面的章节将会对它们逐个进行分析介绍.目录如下: 01. Java多线程系列--"JUC锁"01之 框架 02. Java多线程系列--"JUC锁 ...

  5. Java多线程系列--“JUC锁”03之 公平锁(一)

    概要 本章对"公平锁"的获取锁机制进行介绍(本文的公平锁指的是互斥锁的公平锁),内容包括: 基本概念 ReentrantLock数据结构 参考代码 获取公平锁(基于JDK1.7.0 ...

  6. JUC锁框架——AQS源码分析

    2019独角兽企业重金招聘Python工程师标准>>> JUC锁介绍 Java的并发框架JUC(java.util.concurrent)中锁是最重要的一个工具.因为锁,才能实现正确 ...

  7. JUC并发编程共享模型之无锁(五)

    5.1 问题引出 public interface Account {// 获取余额Integer getBalance();void withdraw(Integer amount);/*** 方法 ...

  8. 【JUC】第五章 JUC 阻塞队列、线程池

    第五章 JUC 阻塞队列.线程池 文章目录 第五章 JUC 阻塞队列.线程池 一.阻塞队列 1.简介 2.BlockingQueue 的方法 3.常见的 BlockingQueue 二.线程池 1.简 ...

  9. JUC系列(五)| Synchonized关键字进一步理解

    多线程一直Java开发中的难点,也是面试中的常客,趁着还有时间,打算巩固一下JUC方面知识,我想机会随处可见,但始终都是留给有准备的人的,希望我们都能加油!!! 沉下去,再浮上来,我想我们会变的不一样 ...

最新文章

  1. .NET笔试题集(五)
  2. ubuntu怎么测tcp协议的服务器,Ubuntu利用TCP协议来获取server时间(示例代码)
  3. Ubuntu terminal路径太深,名字太长
  4. windows命令行无法启动redis_Win10 3分钟简单、快速安装Redis
  5. exists用法_SQL中的ALL、ANY和SOME的用法介绍
  6. EasyUI 分页 简洁代码
  7. Docker新手入门:基本用法
  8. validate+jquery+ajax表单验证
  9. python 网站发送验证码_Python爬虫模拟登录带验证码网站
  10. oracle 并行用索引,分区索引并行导致的性能问题
  11. 在启动时从配置文件中读取对象
  12. 实体-关系信息抽取上线使用F1值87.1% (附数据集)
  13. wamp 局域网访问
  14. python opencv 中文路径_解决python cv2.imread 读取中文路径的图片返回为None的问题
  15. java azure blobs sas_仅使用SAS令牌连接到Azure存储帐户?
  16. android加载海康威视(萤石sdk)摄像头
  17. 机器学习二分类模型评价指标详述
  18. SPSS 市场细分:客户画像\客户价值模型
  19. 人力资源管理专业知识与实务(中级)
  20. canvas 实现会动眼睛的企鹅

热门文章

  1. 使用IDA 进行远程调试
  2. 给Source Insight做个外挂系列之六--“TabSiPlus”的其它问题
  3. NAT类型及检测方法
  4. Android 图形架构
  5. 白话科普系列——网站靠什么提升加载速度?
  6. LeetCode解题的常见模式套路
  7. 音视频技术开发周刊 | 186
  8. 唐敏豪:我给MSU评测打9分
  9. Hotstar赛事直播编码组合优化
  10. 吉长江:基于学习的视频植入技术是未来趋势