首页 > 编程语言 >【Java 并发】【九】【AQS】【八】ReentrantReadWriteLock之ReadLock读锁原理

【Java 并发】【九】【AQS】【八】ReentrantReadWriteLock之ReadLock读锁原理

时间:2023-04-08 12:22:28浏览次数:40  
标签:加锁 Java AQS 次数 ReadLock 读锁 线程 加读 rh

1  前言

上节我们看了下ReentrantReadWriteLock读写锁的写锁的申请和释放过程,这节我们就来看下读锁的。

2  线程读锁记录

回顾一下之前的例子,在读写并发操作的时候,读取数据的时候加读锁:

public class ReentrantReadWriteLockTest {
    // 声明一个读写锁
    private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    // 声明写锁
    private static ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
    // 声明读锁
    private static ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
    // 共享变量
    private static int value = 0;
    // 读取数据操作
    public static int getValue() {
        try {
            // 读取前加读锁
            readLock.lock();
            return value;
        } finally {
            // 释放读锁
            readLock.unlock();
        }
    }

    public static void addValue() {
        try {
            writeLock.lock();
            value++;
        } finally {
            writeLock.unlock();
        }
    }
}

上面的getValue方法就是使用读锁的样例。readLock.lock、readLock.unlock分别对应着读锁的加锁和释放,之前我们讲state 的高16位表示读锁个数,那现在问题来了,每个线程怎么知道自己读锁加了多少次?由于读锁是共享的,所以state变量上表示不出每个线程加读锁的个数。应该是每个线程都会记录自己加了多少个读锁;每个线程都有自己的一份记录,所以这里就用到了ThreadLocal。ReentrantReadWriteLock就是使用ThreadLocal来记录每个线程读锁的个数的。它具体的设计如下所示:

static final class HoldCounter {
    // 当前线程的读锁个数,count的初始值是0
    int count = 0;
    // 当前线程的id
    final long tid = getThreadId(Thread.currentThread());
}

// ThreadLocalHoldCounter 读锁存储容器,这里继承了ThreadLocal
static final class ThreadLocalHoldCounter
    extends ThreadLocal<HoldCounter> {
    public HoldCounter initialValue() {
        return new HoldCounter();
    }
}
// 这里本质是一个ThreadLocal,每个线程通过它可获取自己读锁的个数
private transient ThreadLocalHoldCounter readHolds;

我们接下来就分析下读锁的底层是怎么实现的。

3  读锁加锁lock源码分析

读锁加锁的入口方法源码如下:

public void lock() {
    //调用sync的acquireShared()方法,也还是进入了AQS的acquireShared方法了
    sync.acquireShared(1);
}

这里调用的是Sync同步器的acquireShared方法,最后还是进入了AQS的acquireShared方法:

public final void acquireShared(int arg) {
    // 1.调用子类Sync的tryAcquireShared方法
    // 2. 如果获取读锁失败,则调用doAcquireShared进入等待队列等待
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

获取读锁的入口acquireShared的模板流程:
(1)首先调用子类的tryAcquireShard方法去尝试获取读锁,也就是调用子类Sync的tryAcquireShared方法尝试获取读锁
(2)如果获取读锁成功,直接返回;否则获取失败,调用doAcquireShared方法进入等待队列等待
我们画个图理解一下:

doAcquireShared方法之前讲解AQS的时候已经分析过了,我们来看一下Sync的tryAcquireShared方法。

3.1  Sync的tryAcquireShred源码实现

protected final int tryAcquireShared(int unused) {
    // 获取当前线程
    Thread current = Thread.currentThread();
    // 获取state变量的值
    int c = getState();
    // 计算写锁的次数,如果写锁次数非0,且加写锁的不是自己
    // 说明别人加了写锁,自己这时候获取读锁失败,返回-1
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    // 计算一下读锁的个数
    int r = sharedCount(c);
    // 调用readerShouldBolck,判断是否是公平锁,是否允许加锁
    if (!readerShouldBlock() &&
        // 读锁个数r < 65535,说明读锁个数还剩余
        r < MAX_COUNT &&
        // 执行cas尝试加读锁
        compareAndSetState(c, c + SHARED_UNIT)) {
        // 如果r == 0,说明自己是第一个加读锁的线程
        if (r == 0) {
            // 记录一些第一个加读锁线程
            firstReader = current;
            // 第一个加锁线程加锁次数为1
            firstReaderHoldCount = 1;
        }
        // 如果自己是第一个加读锁的线程,说明之前加锁过了
        // 直接修改一下次数即可
        else if (firstReader == current) {
            firstReaderHoldCount++;
        } else {
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != getThreadId(current))
                // 从ThreadLocal中获取下当前线程加锁的次数
                cachedHoldCounter = rh = readHolds.get();
            // 如果当前线程第一次加锁,设置一下ThreadLocal
            else if (rh.count == 0)
                readHolds.set(rh);
            // 当前线程加锁次数加1即可
            rh.count++;
        }
        return 1;
    }
    // 如果上面CAS操作加锁失败了,进入这个兜底方法
    return fullTryAcquireShared(current);
}

我们画个图来理解一下:

上面的流程图,我们再一步步分析一下:
(1)首先线程进来,先获取锁的记录变量state
(2)计算一下写锁个数,如果写锁个数非零,并且加写锁的线程不是自己,那么由于读写互斥,此时加读锁失败,返回-1
(3)如果写锁个数为零,或者是自己加了写锁,则继续
(4)readShouldBlock根据当前的公平锁、非公平锁、等待队列情况返回是否允许加锁,如果不允许则暂时获取锁失败,进入兜底加锁机制,即执行fullTryAcquireShared方法。
(5)判断读锁的加锁次数,r < MAX_COUNT即65535是否达到上限,如果达到上限则进入兜底加锁机制fullTryAcquireShared。
         未达到上限则执行CAS操作尝试去加读锁,如果CAS加锁失败,也会进入兜底加锁机制fullTryAcquireShared方法
(6)如果CAS加锁成功了,则从ThreadLocal获取当前加读锁的次数,将加读锁的次数+1,就可以了

3.2  fullTryAcquireShared加读锁源码

那我们来看下兜底机制,里面到底是个什么逻辑。

final int fullTryAcquireShared(Thread current) {
    
    HoldCounter rh = null;
    // 在for循环中,不断重试,知道有结果
    for (;;) {
        // 获取读写锁的变量state
        int c = getState();
        // 如果有写锁,并且加锁不是自己
        // 说明别人加了写锁,读写互斥,直接返回失败
        if (exclusiveCount(c) != 0) {
            if (getExclusiveOwnerThread() != current)
                return -1;
        // 根据公平、非公平模式、等待队列等判断是否应该被阻塞
        } else if (readerShouldBlock()) {
            // 如果被阻塞
            // 第一个加读锁的线程是自己,啥也不干
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
            } else {
                if (rh == null) {
                    // 自己不是第一个加读锁的线程
                    // 则获取一下自己加读锁的次数
                    rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current)) {
                        rh = readHolds.get();
                        // 如果加读锁的次数是0,从ThreadLocal从移除
                        if (rh.count == 0)
                            readHolds.remove();
                    }
                }
                // 加读锁次数是0此,此时有应该阻塞,直接返回加锁失败
                if (rh.count == 0)
                    return -1;
            }
        }
        // 如果读锁次数已达上限,抛出异常
        if (sharedCount(c) == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // cas操作尝试加锁,如果cas加锁成功,进入下面的逻辑
        if (compareAndSetState(c, c + SHARED_UNIT)) {
            // 如果自己是第一个加锁的线程,设置一下第一个加锁的人是当前线程
            if (sharedCount(c) == 0) {
                firstReader = current;
                firstReaderHoldCount = 1;
            } else if (firstReader == current) {
                // 第一个加锁的线程是自己,将自己加锁次数+1即可
                firstReaderHoldCount++;
            } else {
                // 这里的操作不外乎就是从ThreadLocal从取出自己加锁的次数,然后将次数+1即可
                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
            }
            // 加锁成功,返回1
            return 1;
        }
    }
}

其实大体上跟tryAcquireShred差不多的,只是在一个for循环里面不断重试而已,提高加锁成功的概率,下面我们也是再画个图来看一下:

上面的流程图,我们再捋一下:
(1)首先获取读写锁状态state,判断如果有别人加了写锁,由于读写互斥,则直接加读锁失败,返回-1
(2)如果别人没有加写锁,判断一下自己是否应该被阻塞(结合公平锁、非公平所、等待队列)。
  如果应该被阻塞,且自己加读锁的次数count == 0,则返回-1,加锁失败。
  如果自己是第一个加读锁的线程,可以网开一面,继续尝试获取读锁,进入for循环重试
(3)如果不应该被阻塞,判断加锁次数是否达到上限,如果达到上限,直接抛出异常
(4)如果读锁次数还有剩余,直接CAS操作尝试加锁,加锁失败则进入for循环重试。
如果加锁成功,则从ThreadLocal中取出之前加锁次数,然后将加锁次数+1,最后返回1,表示本次操作加锁成功。

4  读锁释放锁unlock源码分析

我们接下来继续,讲解读锁ReadLock的释放锁流程:

public void unlock() {
    // 调用的还是Sync同步器的releaseShared方法,也就是AQS的releaseShared方法
    sync.releaseShared(1);
}

这里就是对Sync的releaseShared方法的一个封装底层还是会进入的AQS的releaseShared方法,继续来看AQS的releaseShared模板方法:

public final boolean releaseShared(int arg) {
    // 1. 直接调用到子类的tryReleaseShared方法释放共享锁
    if (tryReleaseShared(arg)) {
        // 2. 如果共享锁释放成功,将共享资源传播,唤醒等待队列的后续节点线程
        doReleaseShared();
        return true;
    }
    return false;
}

这里又回到了之前讲解过的AQS的释放共享资源releaseShared的模板方法里面了:
(1)调用子类Sync的tryReleaseShared方法,去实际释放共享锁
(2)如果释放成功,则调用AQS的doReleaseShared方法唤醒等待队列中的线程,进行共享锁资源的传播,这里之前讲解AQS的时候讲解过了
我们画个图理解下:

核心的释放逻辑还是在类Sync的tryReleaseShared方法里面,我们继续分析。

4.1  Sync的tryReleaseShared源码实现

protected final boolean tryReleaseShared(int unused) {
    // 获取当前线程
    Thread current = Thread.currentThread();
    // 将当前线程加锁次数-1
    if (firstReader == current) {
        // 如果之前只是加了一次锁,那么就释放锁了
        if (firstReaderHoldCount == 1)
            firstReader = null;
        else
        // 如果加了多次锁,锁的次数减少1
            firstReaderHoldCount--;
    } else {
        // 这里的逻辑,就是将ThreadLocal中自己存储的加锁次数减少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;
    }
    // 然后这里就是执行CAS减少加锁的次数,直到成功为止
    for (;;) {
        int c = getState();
        int nextc = c - SHARED_UNIT;
        // cas修改读写锁变量state,将读锁次数-1
        // 注意由于使用高16位表示读锁,所以单位值SHARED_UNIT
        if (compareAndSetState(c, nextc))
            // 判断读锁的个数是否为0,如果为0说明读锁完全释放了
            return nextc == 0;
    }
}

我们画个图来理解一下:

这里就是读锁ReadLock释放锁的流程了,我们再来总结一下:
(1)释放锁首先判断一下,当前线程是否是第一个加锁线程,也就是current == firstReaderHoldCount (因为第一个加读锁的线程,加锁次数存储在firstReaderHoldCount中!!!,后续的加读锁的线程加锁次数存储在ThreadLocal中!!!)
(2)如果自己是第一个加锁线程,扣减firstReaderHolderCount次数,如果扣为零了,则将firstReaderHolderCount 置为null
(3)如果不是第一个加锁线程,从ThreadLocal中取出加锁次数,然后次数扣减1;如果加锁次数为零,从ThreadLocal中移除,因为不需要记录这个线程的加锁次数了,直接释放ThreadLocal的空间。否则还继续保存在ThreadLocal中
(4)执行CAS操作修改state读写锁的状态变量,注意这里由于是高16位表示读锁,所以读锁每减少1,则state 减少 65536
(5)不断重复CAS操作,直到成功为止,判断如果当前读锁个数为0,则读锁完全释放了返回1,否则返回-1

5  小结

到这里ReentrantReadWriteLock中读锁ReadLock加锁、释放锁的底层原理和源码我们就看的差不多了,有理解不对的地方欢迎指正哈。

标签:加锁,Java,AQS,次数,ReadLock,读锁,线程,加读,rh
From: https://www.cnblogs.com/kukuxjx/p/17297702.html

相关文章

  • 【JAVA树根白话二】——继承
    JAVA树根白话二继承Begin……[ABC]继承——面向对象的三个基本特征之一(另外两个是封装、多态) 应用场景:当封装两个类后,第一个类中有一个非常复杂的成员函数,第二个类也需要同样的一个成员函数。如果第二个类重新编写成员函数,会增加开发时间,并且可能会因为一些疏忽,造......
  • 剑指offer03(Java)-数组中重复的数字(简单)
    题目:找出数组中重复的数字。在一个长度为n的数组nums里的所有数字都在0~n-1的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。示例1:输入:[2,3,1,0,2,5,3]输出:2或3 限制:2<=n<=1000......
  • JavaScript遍历数组用splice方法删除元素,这样写可能有遗漏,你遇到过吗?
    在编写“圳品”信息系统中,有时需要对二维数组中的数据进行筛选并删除一些元素,比如删除二维数组中首个元素为0的行。开始是用for循环访问数组+splice方法删除元素来做:vara=newArray([0,0,0,0],[1,1,1,1],[0,2,2,2],[......
  • JavaScript的引入方式
    外部JS文件deno.jsalert('你好!JavaScript');JS引入方式.html<!--方式一:内部脚本--><!--标签不能自闭和--><script>alert('你好JS')</script><!--方式二:外部引入--><scriptsrc="demo.js"></script>......
  • Java方法
    Java方法方法是什么解决一类问题的步骤的有序组合System.out.print()-------------------System是一个类out是一个对象print()是一个方法方法是一个语句块集合,它们在一起执行一个功能设计原则:保持原子性,一个方法只完成一个功能方法的定义及调用Java只有值传递方法......
  • Java内存模型
    Java内存模型的作用《Java虚拟机规范》中曾试图定义一种“Java内存模型”(JavaMemoryModel,JMM)来屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。在此之前,主流程序语言(如C和C++等)直接使用物理硬件和操作系统的内存模......
  • 内存溢出:报错java.lang.OutOfMemoryError: PermGen space
    前言前后台调试过程中某个查询操作导致了后台报错java.lang.OutOfMemoryError:PermGenspace,百度了一下说是内存溢出,设置JVM参数就能解决,确实是如此。引用别人的解释:OutOfMemoryError:PermGenspace非堆溢出(永久保存区域溢出) 这种错误常见在web服务器对JSP进行pre......
  • javaEE进阶小结与回顾(二)
    内部类概述,在一个类里面再定义一个类,这个类称为内部类,外部的类称为外部类分类,成员内部类,局部内部类 成员内部类调用方法:方式一:通过外部类的方法,创建内部类对象方式二:第三方创建内部类对象------Outer.Inneroi=newOuter().newInner(); 修饰符private:只......
  • Java基础知识点(接口1)
    一:接口出现接口的原因:让两个类中的共同行为具有统一性。实质上接口就是一种规则。二:接口与抽象类的异同接口是一种规则,是对行为的抽象。接口的定义和使用:接口用interface关键字来定义publicinterface接口名{}接口不能实例化。接口和类之间是实现关系,通过Implements关键字来表示......
  • 链表的回文判断—Java实现
    对于这个题,主要是老是局限于方法内的变量,未想到借助外部变量辅助:具如下,不可用数除法,会溢出异常:即使是取最大的long也会溢出,常规方法不再赘述,具体以代码如下:1packageProblemSolve;23publicclassSolution5{4privateListNodefrontNode;5publicboolean......