当前位置: 首页 > news >正文

ReetrantReadWriteLock底层原理

文章目录

    • 一、读写锁介绍
    • 二、ReentrantReadWriteLock底层原理
      • 1. 读写锁的设计

一、读写锁介绍

现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁(读多写少)。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源(读读可以并发);但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写操作了(读写,写读,写写互斥)。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。

针对这种场景,JAVA的并发包提供了读写锁ReentrantReadWriteLock,它内部,维护了 一对相关的锁,一个用于只读操作,称为读锁;一个用于写入操作,称为写锁。

线程进入读锁的前提条件:

  • 没有其它线程的写锁
  • 没有写请求或者有写请求,但是调用线程和持有锁的线程是同一个

线程进入写锁的前提条件:

  • 没有其他线程的读锁
  • 没有其他线程的写锁

读写锁有以下三个重要的特性:

  • 公平选择性:支持非公平和公平的锁获取方式,吞吐量还是非公平优于公平
  • 可重入:读锁和写锁都支持线程重入,以读写线程为例,读锁获取读锁后,能够再次获取读锁,写线程在获取写锁之后能够再次获取写锁,同时也可以获取读锁。
  • 锁降级:遵循获取写锁,再获取读锁最后释放写锁的次序,写锁能够降级为读锁。

二、ReentrantReadWriteLock底层原理

看源码需要了解三个核心问题

  1. 读写锁是怎样实现分别记录读写状态的?
  2. 写锁时怎么获取和释放的?
  3. 读锁时怎么获取和释放的?

1. 读写锁的设计

在这里插入图片描述
首先看它的类信息:

public class ReentrantReadWriteLockimplements ReadWriteLock, java.io.Serializable {}

可以发现该类实现了ReadWriteLock这个接口

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

该接口就实现了读锁和写锁的规范,以下就是相关的类图

在这里插入图片描述
下面看看ReentrantReadWriteLock的读写锁的实现逻辑,首先看写锁:

public static class ReadLock implements Lock, java.io.Serializable {private static final long serialVersionUID = -5992448646407690164L;private final Sync sync;protected ReadLock(ReentrantReadWriteLock lock) {sync = lock.sync;}public void lock() {sync.acquireShared(1);}public void lockInterruptibly() throws InterruptedException {sync.acquireSharedInterruptibly(1);}public boolean tryLock() {return sync.tryReadLock();}public boolean tryLock(long timeout, TimeUnit unit)throws InterruptedException {return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));}public void unlock() {sync.releaseShared(1);}public Condition newCondition() {throw new UnsupportedOperationException();}public String toString() {int r = sync.getReadLockCount();return super.toString() +"[Read locks = " + r + "]";}}
  • ReadLock:是一个ReetrantReadWriteLock的静态内部类
  • Sync:和ReentrantLock一样,Sync也是ReetrantReadWriteLock的一个静态内部抽象类,它继承了AbstractQueuedSynchronizer,实现了AQS的逻辑,然后它会有两种实现,分别是FairSync公平锁,和NonfairSync非公平锁
  • lock:可以发现加读锁加的是AQS的共享锁
  • tryLock:尝试获取读锁

然后看看写锁是怎么实现的

 public static class WriteLock implements Lock, java.io.Serializable {private static final long serialVersionUID = -4992448646407690164L;private final Sync 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);}public Condition newCondition() {return sync.newCondition();}public String toString() {Thread o = sync.getOwner();return super.toString() + ((o == null) ?"[Unlocked]" :"[Locked by thread " + o.getName() + "]");}public boolean isHeldByCurrentThread() {return sync.isHeldExclusively();}public int getHoldCount() {return sync.getWriteHoldCount();}}

下面我们需要思考一个核心问题,读写锁的状态是怎么用AQS底层的state状态来维护的。其实这里的核心问题就是,如何用一个变量维护多种状态。在 ReentrantLock 中,使用 Sync ( 实际是 AQS )的 int 类型的 state 来表示同步状态,表示锁被一个线程重复获取的次数。但是,读写锁 ReentrantReadWriteLock 内部维护着一对读写锁,如果要用一个变量维护多种状态,需要采用“按位切割使用”的方式来维护这个变量,将其切分为两部分:高16为表示读,低16为表示写。分割之后,读写锁是如何迅速确定读锁和写锁的状态呢? 其实底层是通过位运算来实现的。假如当前同步状态为S, 那么写状态,等于 S & 0x0000FFFF(将高 16 位全部抹去)。 当写状态加1,等于S+1。读状态,等于 S >>> 16 (无符号补 0 右移 16 位)。当读状态加1,等于 S+(1<<16),也就是S+0x00010000。根据状态的划分能得出一个推论:S不等于0时,当写状态(S&0x0000FFFF)等于0时,则读状态(S>>>16)大于0,即读锁已被获取。
在这里插入图片描述
源码如下:

//该部分在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; }
  • exclusiveCount:获得持有写状态的锁的次数
  • sharedCount:获得持有读状态的锁的线程数量,不同于写锁,读锁利用同时被多个线程持有,而每个线程持有的读锁支持重入的特性,所以需要每个线程持有的读锁数量单独计数,这就需要HoldCounter计数器。

HoldCounter计数器
读锁的内在机制其实就是一个共享锁。一次共享锁的操作就相当于对HoldCounter 计数器的操作。获取共享锁,则该计数器 + 1,释放共享锁,该计数器 - 1。只有当线程获取共享锁后才能对共享锁进行释放、重入操作。

static final class HoldCounter {int count = 0;// Use id, not reference, to avoid garbage retentionfinal long tid = getThreadId(Thread.currentThread());}static final class ThreadLocalHoldCounterextends ThreadLocal<HoldCounter> {public HoldCounter initialValue() {return new HoldCounter();}}

写锁的获取
写锁是一个支持重进入的排它锁。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程, 则当前线程进入等待状态。

protected final boolean tryAcquire(int acquires) {
//获取当前线程Thread current = Thread.currentThread();//获取当前state的值int c = getState();//获取写锁的重入次数int w = exclusiveCount(c);//state!=0则表示当前有写锁或读锁if (c != 0) {//判断是否是重入if (w == 0 || current != getExclusiveOwnerThread())return false;if (w + exclusiveCount(acquires) > MAX_COUNT)throw new Error("Maximum lock count exceeded");//获取重入锁setState(c + acquires);return true;}// writerShouldBlock有公平与非公平的实现, 非公平返回false,会尝试通过cas加锁,c==0 写锁未被任何线程获取,当前线程是否阻塞或者cas尝试获取锁if (writerShouldBlock() ||!compareAndSetState(c, c + acquires))return false;//设置锁由当前线程独占setExclusiveOwnerThread(current);return true;}

上面简单的代码就实现了下面的逻辑:

  • 读写互斥
  • 写写互斥
  • 写锁支持同一个线程重入
  • writeShouldBlock写锁是否阻塞实现取决公平与非公平的策略
    在这里插入图片描述

写锁的释放

     protected final boolean tryRelease(int releases) {if (!isHeldExclusively())throw new IllegalMonitorStateException();//设置状态int nextc = getState() - releases;boolean free = exclusiveCount(nextc) == 0;//如果state为0,表示释放写锁if (free)setExclusiveOwnerThread(null);setState(nextc);return free;}

在这里插入图片描述
读锁的获取

protected final int tryAcquireShared(int unused) {//获取当前线程Thread current = Thread.currentThread();//获取stateint c = getState();//exclusiveCount(c) != 0 判断是否有写锁//getExclusiveOwnerThread() != current),判断当前线程是否是写锁的持有者if (exclusiveCount(c) != 0 &&getExclusiveOwnerThread() != current)return -1;int r = sharedCount(c);if (!readerShouldBlock() &&r < MAX_COUNT &&//cas加读锁compareAndSetState(c, c + SHARED_UNIT)) {//r==0表示第一次获取读锁if (r == 0) {//设置第一个读为当前线程firstReader = current;//设置当前读锁的重入次数firstReaderHoldCount = 1;} else if (firstReader == current) {          //第一个读的重入firstReaderHoldCount++;} else {//若不是第一个读,则用HoldCounter记录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);//第一次读锁获取失败,再次尝试(fullTryAcquireShared)}

上面代码的逻辑就实现了:

  • 读锁共享,读读不互斥
  • 读锁可重入,每个获取读锁的线程都会记录对应的重入数
  • 读写互斥,锁降级场景除外
  • 支持锁降级,持有写锁的线程,可以获取读锁,但是后续要记得把读锁和写锁读释放
  • readerShouldBlock读锁是否阻塞实现取决公平与非公平的策略(FairSync和NonfairSync)

在这里插入图片描述
读锁的释放

protected final boolean tryReleaseShared(int unused) {Thread current = Thread.currentThread();if (firstReader == current) {// assert firstReaderHoldCount > 0;if (firstReaderHoldCount == 1)firstReader = null;elsefirstReaderHoldCount--;} 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;}for (;;) {int c = getState();int nextc = c - SHARED_UNIT;if (compareAndSetState(c, nextc))return nextc == 0;}}

在这里插入图片描述

http://www.lryc.cn/news/257895.html

相关文章:

  • LeetCode力扣每日一题(Java):35、搜索插入位置
  • Unity中结构体定义的成员如何显示在窗口中
  • Python3开发环境的搭建
  • Leetcode 2957. Remove Adjacent Almost-Equal Characters
  • 透析跳跃游戏
  • 贵州开放大学形成性考核 平时作业 参考试题
  • Leetcode 2962. Count Subarrays Where Max Element Appears at Least K Times
  • Mybatis XML 配置文件
  • CCF计算机软件能力认证202309-1坐标变换(其一)(C语言)
  • k8s 如何部署Mysql(史上最权威教程)?
  • 红队攻防实战之Redis-RCE集锦
  • 六级翻译之印章
  • PHP数据库操作实例 - 学生信息管理
  • 企业架构LB-服务器的负载均衡之LVS实现
  • Java程序设计基础 - 课程概述
  • 基于SpringBoot+Vue前后端分离的商城管理系统(Java毕业设计)
  • vue3中实现el-tree通过ctrl或shift批量选择节点并高亮展示
  • HarmonyOS 振动效果开发指导
  • 【ACM独立出版、确定的ISBN号】第三届密码学、网络安全和通信技术国际会议(CNSCT 2024)
  • Qt12.8
  • QT使用SQLite 超详细(增删改查、包括对大量数据快速存储和更新)
  • 基于Springboot+mybatis+mysql+jsp招聘网站
  • PHP介绍及安装
  • linux C++监听管道文件方式
  • 【Qt开发流程】之UI风格、预览及QPalette使用
  • 数组实现循环队列(增设队列大小size)
  • [BJDCTF2020]EzPHP 许多的特性
  • Ubuntu开机出现Welcome to emergency mode解决办法
  • Android 7.1 默认自拍镜像
  • 设计模式(二)-创建者模式(5)-建造者模式