读写锁实现逻辑相对比较复杂,但是却是一个经常使用到的功能,希望将我对ReentrantReadWriteLock的源码的理解记录下来,可以对大家有帮助

前提条件

在理解ReentrantReadWriteLock时需要具备一些基本的知识

理解AQS的实现原理

之前有写过一篇《深入浅出AQS源码解析》关于AQS的文章,对AQS原理不了解的同学可以先看一下

理解ReentrantLock的实现原理

ReentrantLock的实现原理可以参考《深入浅出ReentrantLock源码解析》

什么是读锁和写锁

对于资源的访问就两种形式:要么是读操作,要么是写操作。读写锁是将被锁保护的临界资源的读操作和写操作分开,允许同时有多个线程同时对临界资源进行读操作,任意时刻只允许一个线程对资源进行写操作。简单的说,对与读操作采用的是共享锁,对于写操作采用的是排他锁

读写状态的设计

ReentrantReadWriteLock是用state字段来表示读写锁重复获取资源的次数,高16位用来标记读锁的同步状态,低16位用来标记写锁的同步状态

// 划分的边界线,用16位来划分
static final int SHARED_SHIFT = 16;
// 读锁的基本单位,也就是读锁加1或者减1的基本单位(1左移16位后的值)
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
// 读写锁的最大值(在计算读锁的时候需要先右移16位)
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
// 写锁的掩码,state值与掩码做与运算后得到写锁的真实值
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; // 获取资源被读锁占用的次数
static int sharedCount(int c){
return c >>> SHARED_SHIFT;
} // 获取资源被写锁占用的次数
static int exclusiveCount(int c){
return c & EXCLUSIVE_MASK;
}

在统计读锁被每个线程持有的次数时,ReentrantReadWriteLock采用的是HoldCounter来实现的,具体如下:

// 持有读锁的线程重入的次数
static final class HoldCounter {
// 重入的次数
int count = 0;
// 持有读锁线程的线程id
final long tid = getThreadId(Thread.currentThread());
} /**
* 采用ThreadLocal机制,做到线程之间的隔离
*/
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
} /**
* 线程持有可重入读锁的次数
*/
private transient ThreadLocalHoldCounter readHolds; /**
* 缓存最后一个成功获取读锁的线程的重入次数,有两方面的好处:
* 1、避免了通过访问ThreadLocal来获取读锁的信息,这个优化的前提是
* 假设多数情况下,一个获取读锁的线程,使用完以后就会释放读锁,
* 也就是说最后获取读锁的线程和最先释放读锁的线程大多数情况下是同一个线程
* 2、因为ThreadLocal中的key是一个弱引用类型,当有一个变量持有HoldCounter对象时,
* ThreadLocalHolderCounter中最后一个获取锁的线程信息不会被GC回收掉
*/
private transient HoldCounter cachedHoldCounter; /**
* 第一个获取读锁的线程,有两方面的考虑:
* 1、记录将共享数量从0变成1的线程
* 2、对于无竞争的读锁来说进行线程重入次数数据的追踪的成本是比较低的
*/
private transient Thread firstReader = null; /**
* 第一个获取读锁线程的重入次数,可以参考firstReader的解析
*/
private transient int firstReaderHoldCount;

ReentrantReadWriteLock 源码解析

ReentrantReadWriteLock 一共有5个内部类,具体如下:

  • Sync:公平锁和非公平锁的抽象类
  • NonfairSync:非公平锁的具体实现
  • FairSync:公平锁的具体实现
  • ReadLock:读锁的具体实现
  • WriteLock:写锁的具体实现

我们从读锁ReadLock和写锁WriteLock的源码开始分析,然后顺着这个思路将整个ReentrantReadWriteLock中所有的核心源码(所有的包括内部类)进行分析。

ReadLock类源码解析

public static class ReadLock implements Lock, java.io.Serializable {

    private final Sync sync;

    /**
* 通过ReentrantReadWriteLock中的公平锁或非公平锁来初始化sync变量
*/
protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
} /**
* 阻塞的方式获取锁,因为读锁是共享锁,所以调用acquireShared方法
*/
public void lock() {
sync.acquireShared(1);
} /**
* 可中断且阻塞的方式获取锁
*/
public void lockInterruptibly() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
} /**
* 超时尝试获取锁,非阻塞的方式
*/
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
/**
* 尝试获取写锁,非阻塞的方式
*/
public boolean tryLock() {
return sync.tryReadLock();
} /**
* 释放锁
*/
public void unlock() {
sync.releaseShared(1);
}
}

接下来,我们重点看一下在公平锁和非公平锁下Sync.acquireSharedSync.releaseSharedSync.tryLock这3个方法的实现(acquireSharedInterruptiblytryAcquireSharedNanos是AQS中的方法,这里就不在讨论了,具体可以参考《深入浅出AQS源码解析》),其中Sync.acquireShared中核心调用的方法是Sync.tryAcquireSharedSync. releaseShared中核心调用的方法是Sync.tryReleaseSharedSync. tryLock中核心调用的方法是Sync.tryReadLock,所以我们重点分析Sync.tryAcquireShared方法、Sync.tryReleaseShared方法和sync.tryReadLock方法

Sync.tryAcquireShared方法

protected final int tryAcquireShared(int unused) {
/**
* 以共享锁的方式尝试获取读锁,步骤如下:
* 1、如果资源已经被写锁获取了,直接返回失败
* 2、如果读锁不需要等待(公平锁和非公平锁的具体实现有区别)、
* 并且读锁未超过上限、同时设置读锁的state值成功,则返回成功
* 3、如果步骤2失败了,需要进入fullTryAcquireShared函数再次尝试获取读锁
*/
Thread current = Thread.currentThread();
int c = getState();
/**
* 资源已经被写锁独占,直接返回false
*/
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount(c);
/**
* 1、读锁不需要等待
* 2、读锁未超过上限
* 3、设置读锁的state值成功
* 则返回成功
*/
if (!readerShouldBlock() &&
r < MAX_COUNT &&
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 != LockSupport.getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
/**
* 如果CAS失败再次获取读锁
*/
return fullTryAcquireShared(current);
}

接下来看一下fullTryAcquireShared方法:

final int fullTryAcquireShared(Thread current) {
/**
* 调用该方法的线程都是希望获取读锁的线程,有3种情况:
* 1、在尝试通过CAS操作修改state时由于有多个竞争读锁的线程导致CAS操作失败
* 2、需要排队等待获取读锁的线程(公平锁)
* 3、超过读锁限制的最大申请次数的线程
*/
HoldCounter rh = null;
for (;;) { // 无限循环获取锁
int c = getState();
// 已经被写线程获取锁了,直接返回
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
// 需要被block的读线程(公平锁)
} else if (readerShouldBlock()) {
// 如果时当前线程
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
} else {
// 清理当前线程中重入次数为0的数据
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");
// 修改读锁的state值
if (compareAndSetState(c, c + SHARED_UNIT)) {
// 最新获取到读锁的线程设置相关的信息
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++; // 当前线程重复获取锁(重入)
} else {
// 在readHolds中记录获取锁的线程的信息
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;
}
}
}

Sync.tryReleaseShared方法

tryReleaseShared方法的实现逻辑比较简单,我们直接看代码中的注释

protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
/**
* 如果当前线程是第一个获取读锁的线程,有两种情况:
* 1、如果持有锁的次数为1,直接释放成功
* 2、如果持有锁的次数大于1,说明有重入的情况,需要次数减1
*/
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
/**
* 如果当前线程不是第一个获取读锁的线程
* 需要更新线程持有锁的重入次数
* 如果次数小于等于0说明有异常,因为只有当前线程才会出现持有锁的重入次数等于0或者1
*/
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;
}
// 修改state的值
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// 如果是最后一个释放读锁的线程nextc为0,否则不是
return nextc == 0;
}
}

sync.tryReadLock方法

tryReadLock的代码比较简单,就直接在将解析过程在注释中描述

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");
/**
* 通过CAS操作设置state的值,如果成功表示尝试获取读锁成功,需要做以下几件事情:
* 1、如果是第一获取读锁要记录第一个获取读锁的线程信息
* 2、如果是当前获取锁的线程和第一次获取锁的线程相同,需要更新第一获取线程的重入次数
* 3、更新获取读锁线程相关的信息
*/
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;
}
}
}

WriteLock类源码解析

public static class WriteLock implements Lock, java.io.Serializable {
private final Sync sync; /**
* 通过ReentrantReadWriteLock中的公平锁或非公平锁来初始化sync变量
*/
protected WriteLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
} /**
* 阻塞的方式获取写锁
*/
public void lock() {
sync.acquire(1);
} /**
* 中断的方式获取写锁
*/
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
} /**
* 尝试获取写锁
*/
public boolean tryLock( ) {
return sync.tryWriteLock();
} /**
* 超时尝试获取写锁
*/
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
} /**
* 释放写锁
*/
public void unlock() {
sync.release(1);
}
}

接下来,我们重点看一下在公平锁和非公平锁下Sync.tryAcquireSync.tryReleaseSync.tryWriteLock这几个核心方法是如何实现写锁的功能

Sync.tryAcquire方法

Sync.tryAcquire方法的逻辑比较简单,就直接在代码中注释,具体如下:

protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
// 读写锁的次数
int c = getState();
// 写锁的次数
int w = exclusiveCount(c);
/*
* 如果读写锁的次数不为0,说明锁可能有以下3中情况:
* 1、全部是读线程占用资源
* 2. 全部是写线程占用资源
* 3. 读写线程都占用了资源(锁降级:持有写锁的线程可以去持有读锁),但是读写线程都是同一个线程
*/
if (c != 0) {
// 写线程不占用资源,第一个获取锁的线程也不是当前线程,直接获取失败
if (w == 0 || current != getExclusiveOwnerThread())
return false;
// 检查获取写锁的线程是否超过了最大的重入次数
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 修改state的状态,之所以没有用CAS操作来修改,是因为写线程只有一个,是独占的
setState(c + acquires);
return true;
}
/*
* 写线程是第一个竞争锁资源的线程
* 如果写线程需要等待(公平锁的情况),或者
* 写线程的state设置失败,直接返回false
*/
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
// 设置当前线程为owner
setExclusiveOwnerThread(current);
return true;
}

Sync.tryRelease方法

Sync.tryRelease方便的代码很简单,直接看代码中的注释

protected final boolean tryRelease(int releases) {
// 如果释放锁的线程不持有锁,返回失败
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 获取写锁的重入的次数
int nextc = getState() - releases;
// 如果次数为0,需要释放锁的owner
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null); // 释放锁的owner
setState(nextc);
return free;
}

Sync.tryWriteLock方法

Sync.tryWriteLock这个方法也比较简单,就直接上代码了

final boolean tryWriteLock() {
Thread current = Thread.currentThread();
int c = getState();
// 读锁或者写锁已经被线程持有
if (c != 0) {
int w = exclusiveCount(c);
// 写锁第一次获取锁或者当前线程不是第一次获取写锁的线程(也就是不是owner),直接失败
if (w == 0 || current != getExclusiveOwnerThread())
return false;
// 超出写锁的最大次数,直接失败
if (w == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
}
// 竞争写锁的线程修改state,
// 如果成功将自己设置成锁的owner,
// 如果失败直接返回
if (!compareAndSetState(c, c + 1))
return false;
setExclusiveOwnerThread(current); // 设置当前线程持有锁
return true;
}

总结

  • 读锁和写锁的占用(重入)次数都是共用state字段,高位记录读锁,地位记录写锁,所以读锁和写锁的最大占用次数为2^16
  • 读锁和写锁都是可重入的
  • 读锁是共享锁,允许多个线程获取
  • 写锁是排他锁,只允许一个线程获取
  • 一个线程获取了读锁,在非公平锁的情况下,其他等待获取读锁的线程都可以尝试获取读锁,在公平锁的情况下,按照AQS同步队列的顺利来获取,如果队列前面有一个等待写锁的线程在排队,则后面所有等待获取读锁的线程都将无法获取读锁
  • 获取读锁的线程,不能再去申请获取写锁
  • 一个获取了写锁的线程,在持有锁的时候可以去申请获取读锁,在释放写锁以后,还会继续持有读锁,这就是所谓的锁降级
  • 读锁无法升级为写锁,原因是获取读锁的线程可能是多个,而写锁是独占的,不能多个线程持有,也就是说不支持锁升级

深入浅出ReentrantReadWriteLock源码解析的更多相关文章

  1. 深入浅出ReentrantLock源码解析

    ReentrantLock不但是可重入锁,而且还是公平或非公平锁,在工作中会经常使用到,将自己对这两种锁的理解记录下来,希望对大家有帮助. 前提条件 在理解ReentrantLock时需要具备一些基本 ...

  2. 深入浅出Semaphore源码解析

    Semaphore通过permits的值来限制线程访问临界资源的总数,属于有限制次数的共享锁,不支持重入. 前提条件 在理解Semaphore时需要具备一些基本的知识: 理解AQS的实现原理 之前有写 ...

  3. 死磕 java同步系列之ReentrantReadWriteLock源码解析

    问题 (1)读写锁是什么? (2)读写锁具有哪些特性? (3)ReentrantReadWriteLock是怎么实现读写锁的? (4)如何使用ReentrantReadWriteLock实现高效安全的 ...

  4. Java并发之ReentrantReadWriteLock源码解析(一)

    ReentrantReadWriteLock 前情提要:在学习本章前,需要先了解笔者先前讲解过的ReentrantLock源码解析和Semaphore源码解析,这两章介绍了很多方法都是本章的铺垫.下面 ...

  5. Java并发之ReentrantReadWriteLock源码解析(二)

    先前,笔者和大家一起了解了ReentrantReadWriteLock的写锁实现,其实写锁本身实现的逻辑很少,基本上还是复用AQS内部的等待队列思想.下面,我们来看看ReentrantReadWrit ...

  6. 深入浅出AQS源码解析

    最近一直在研究AQS的源码,希望可以更深刻的理解AQS的实现原理.虽然网上有很多关于AQS的源码分析,但是看完以后感觉还是一知半解.于是,我将自己的整个理解过程记录下来了,希望对大家有所帮助. 基本原 ...

  7. 死磕 java同步系列之CyclicBarrier源码解析——有图有真相

    问题 (1)CyclicBarrier是什么? (2)CyclicBarrier具有什么特性? (3)CyclicBarrier与CountDownLatch的对比? 简介 CyclicBarrier ...

  8. 死磕 java同步系列之Phaser源码解析

    问题 (1)Phaser是什么? (2)Phaser具有哪些特性? (3)Phaser相对于CyclicBarrier和CountDownLatch的优势? 简介 Phaser,翻译为阶段,它适用于这 ...

  9. 死磕 java同步系列之StampedLock源码解析

    问题 (1)StampedLock是什么? (2)StampedLock具有什么特性? (3)StampedLock是否支持可重入? (4)StampedLock与ReentrantReadWrite ...

随机推荐

  1. ca77a_c++__一个打开并检查文件输入的程序_流对象_操作文件

    /*ca77a_c++__一个打开并检查文件输入的程序 习题:8.13 8.14*/ /*ca77a_c++__一个打开并检查文件输入的程序 习题:8.13 8.14 */ #include < ...

  2. 重识Java8函数式编程

    前言 最近真的是太忙忙忙忙忙了,很久没有更新文章了.最近工作中看到了几段关于函数式编程的代码,但是有点费解,于是就准备总结一下函数式编程.很多东西很简单,但是如果不总结,可能会被它的各种变体所困扰.接 ...

  3. redis编译报错总结

    redis编译报错总结: 1.不能编译没有GCC 编译工具安装报错:问题1:make时可能会报如下错误cc -c -std=c99 -pedantic -O2 -Wall -W   -g -rdyna ...

  4. Zookeeper客户端Apache Curator

    本文不对Zookeeper进行介绍,主要介绍Curator怎么操作Zookeeper. Apache Curator是Apache ZooKeeper的Java / JVM客户端库,Apache Zo ...

  5. SpringCloud教程第6篇:config(F版本)

    一.简介 在分布式系统中,由于服务数量巨多,为了方便服务配置文件统一管理,实时更新,所以需要分布式配置中心组件.在Spring Cloud中,有分布式配置中心组件spring cloud config ...

  6. Spark学习笔记(三)-Spark Streaming

    Spark Streaming支持实时数据流的可扩展(scalable).高吞吐(high-throughput).容错(fault-tolerant)的流处理(stream processing). ...

  7. VM363:1 Uncaught SyntaxError: Invalid or unexpected token

    此报错主要是因为json字符串转json对象时,json字符串中出现特殊字符(如:换行符)报错. json字符转json对象(如下写则报错) 更改后 参考地址: https://www.cnblogs ...

  8. Python 简明教程 --- 15,Python 函数

    微信公众号:码农充电站pro 个人主页:https://codeshellme.github.io 测试只能证明程序有错误,而不能证明程序没有错误. -- Edsger Dijkstra 目录 本节我 ...

  9. 如何获取自定义meta标签信息?

    <meta name="apple-itunes-app" content="app-id=432274380" /> 类似于这种meta信息,js ...

  10. 二.3.token认证,jwt认证,前端框架

    一.token: 铺垫: 之前用的是通过最基本的用户名密码登录我的运维平台http://127.0.0.1:8000/---这种用的是form表单,但是这种对于前后端分离的不适合.前后端分离,应该通过 ...