首页 > 编程语言 >【Java 并发】【九】【AQS】【八】ReentrantReadWriteLock 读写锁怎么表示

【Java 并发】【九】【AQS】【八】ReentrantReadWriteLock 读写锁怎么表示

时间:2023-04-07 22:11:17浏览次数:56  
标签:Java AQS ReentrantReadWriteLock 写锁 16 int 读锁 static

1  前言

接下来我们来看看ReentrantReadWriteLock读写锁,也是基于之前讲解的AQS来实现的,建立在AQS体系之上的一个并发工具类,这个锁很重要,在很多开源的中间件中使用的非常广泛,很多场景使用它来减少并发操作中的锁冲突,提升并发能力。

2  ReentrantReadWriteLock介绍

ReentrantReadwriteLock里面同时封装了读锁和写锁,分别为ReadLock、WriteLock。
锁的特点是:同时并发操作的时候、读读不互斥,是可以共享的,但是读写、写写操作是互斥的。它主要是通过读读操作不互斥,来减少锁的冲突,提升并发的性能。
我们还是写个例子,感受下:

public class ReentrantReadWriteLockTest {

    private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    // 读锁
    private static ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
    // 写锁
    private static ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
    private static int value = 0;
    // 读取value的时候加读锁
    public static int readValue() {
        try {
            readLock.lock();
            return value;
        } finally {
            readLock.unlock();
        }
    }
    // 修改value的时候加写锁
    public static void addValue() {
        try {
            writeLock.lock();
            value++;
        } finally {
            writeLock.unlock();
        }
    }
    public static class ReadThread extends Thread {
        @Override
        public void run() {
            for (int i = 0 ; i < 300; i++) {
                System.out.println(readValue());
            }
        }
    }
    public static class WriteThread extends Thread{
        @Override
        public void run() {
            for (int i = 0 ; i < 100; i++) {
                addValue();
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        // 两个读线程,读读不互斥
        ReadThread readThread1 = new ReadThread();
        ReadThread readThread2 = new ReadThread();
        // 一个写线程
        WriteThread writeThread = new WriteThread();
        readThread1.start();
        readThread2.start();
        writeThread.start();
        // 等待子线程都执行完
        readThread1.join();
        readThread2.join();
        writeThread.join();
        System.out.println("结束");
    }
}

读取数据的时候加读锁、修改数据的时候加写锁。两个线程readThread1、readThread2读取数据的时候加读锁,这个是可以共享的。这样可以减少一部分锁冲突,提升整体的并发能力。
那么接下来我们就来看看它是怎么实现读锁、怎么实现写锁的吗?以及怎么实现读写互斥、写写互斥的。

3  读锁、写锁分别使用什么来表示

3.1  内部属性

我们先来看看ReentrantReadWriteLock 内部有什么属性:

public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
    // 读锁
    private final ReentrantReadWriteLock.ReadLock readerLock;
    // 写锁
    private final ReentrantReadWriteLock.WriteLock writerLock;
    // 同步器,读锁、写锁都是基于这个同步器来进行封装的
    final Sync sync;
}

每个属性的作用:
readLock:读锁,这里就是ReentrantReadWriteLock提供的一把读锁。
writeLock:写锁,这里就是ReentrantReadWriteLock提供的一把写锁。
sync:同步器,继承自AQS,读写锁的逻辑由Sync同步器来实现,上面的读锁、写锁只是对它的封装而已。

3.2  ReentrantReadWriteLock 构造函数

我们再来看一下ReentrantReadWriteLock的构造函数:

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

看上面的构造函数中,sync居然也是有公平锁FairSync、NonfairSync非公平锁的概念。
如果默认构造函数传递boolean fair = false,也就是默认是非公平锁,同时构造函数中,会同时创建一把读锁ReadLock、一把写锁WriteLock。

3.3  ReentrantReadWriteLock内部类结构

ReenatrantReadWriteLock内部有一把读锁、一把写锁,还有一个同步器Sync(公平模式、非公平模式)。现在我们先从整体上看一下ReentrantReadWriteLock内部类结构。
它的内部类结构跟我们之前讲过的ReentrantLock、Semaphore非常类似,也是有公平锁FairSync、非公平锁NonfairSync,并且它们都是继承自Sync,而Sync又继承了AQS,底层都是基于AQS来实现的。

其实ReentrantReadWriteLock的大部分逻辑都是封装在了Sync这个同步器里面了,FairSync、NonfairSync这两个子类只是封装了公平、非公平的实现而已。
之前我们讲过很多次了,所谓公平的实现就是获取锁之前,查看AQS等待队列是否有人在排队,如果有人在排队,则自己不获取锁,去队列中等待。非公平的实现就是,上来就抢,不管有没有人在排队,抢到就返回,抢不到就去等待队列排队。

3.4  读锁、写锁的表示

上面我们大概了解了ReentrantReadWriteLock内部的属性、类结构,大致总结如下:
(1)属性:有一把读锁readLock、一把写锁writeLock,一个抽象同步器Sync,其中锁的大部分逻辑都是封装在Sync这个抽象同步器里面;ReadLock、WriteLock都是对Sync进行了封装而已。
(2)锁模式和类结构:ReentrantReadWriteLock有公平锁、非公平锁两种模式,具体是根据FairSync、NonfairSync这两个同步工具类封装的,而FairSync、NonfairSync这两个同步工具类又继承自Sync,读写锁的逻辑大多数都封装在Sync里面。
我们接下来看看ReentrantReadWriteLock分别使用什么表示读锁,使用什么表示写锁,具体就在Sync这个抽象同步器里面:

abstract static class Sync extends AbstractQueuedSynchronizer {
    // 共享锁(读锁)的偏移量16位
    static final int SHARED_SHIFT = 16;
    // 共享锁的单位
    static final int SHARED_UNIT = (1 << SHARED_SHIFT);
    // 共享锁的最大个数,2的16次方-1 = 65535
    static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
    // 独占锁掩码,65535,转化为二进制为 0000 0000 0000 0000 1111 1111 1111 1111
    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; }
}

int类型的32位数字同时表示写锁和读锁(高16位读锁、低16位写锁),为什么这么涉及有什么精妙之处么,我们来看下:

ReentrantReadWriteLock使用一个4个字节int 类型的数字同时表示读锁、写锁,int类型数字是4个字节,也就是32位,其中高16位表示读锁,低16位表示写锁。如下图所示:

这样那使用一个32位的数字,高16位表示读锁个数,低16位表示写锁个数;那我怎么知道当前加了多少个读锁,有没有人加写锁呢?
起始这就非常考究位运算了,并且位运算的效率是比较高的。我们来细细看下:
0000 0000 1010 0000 0000 0000 0110 1100,那么可以这样进行运算得到高低16位各自的加锁个数:

读锁的计算,直接将int类型的数字进行无符号右移16位即可:

对应到底层的方法源码就是:

static int sharedCount(int c)    {
    // 这里的SHARD_SHIFT就是16
    // 就是将c进行无服务右移16位,得到读锁个数
    return c >>> SHARED_SHIFT;
}

由于使用高16位表示读锁,所以读锁的个数最多为:2的16次方 - 1 = 65535。也就是如下变量:

// 高16位全部为1的时候, 1111 1111 1111 1111 也就是最大读锁个数
// 转化为十进制为65535个
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;

看完读锁的,那我们再来看看写锁的:

计算写锁的个数,也就是计算int类型的数字中低16位的结果是多少,也就是需要保留低16位的值,高16位全部置为0;对应到位运算的逻辑就是如下代码:

static int exclusiveCount(int c) {
    // 只需要于写锁掩码 0000 0000 0000 0000 1111 1111 1111 1111
    // 进行按位 & 运算即可
    return c & EXCLUSIVE_MASK;
}

后面我们要讲解的线程池中,也是使用一个int类型的数字能同时表示线程池的状态,线程池中线程个数,高3位表示线程池状态,低29位表示线程池中线程个数,跟这里类似。

4  小结

这节我们先把ReentrantReadWriteLock读写锁内部的属性以及结构和读写锁的个数表示看了,下节我们就具体来看看读写锁的使用和原理分析,有理解不对的地方欢迎指正哈。

标签:Java,AQS,ReentrantReadWriteLock,写锁,16,int,读锁,static
From: https://www.cnblogs.com/kukuxjx/p/17297378.html

相关文章

  • Java入门
    一、Java特性和优势1.简单性:不用像C语言那样引用头文件,抛弃了指针2.面向对象3.可移植性:可以跨平台移植,一次编写多次运行4.高性能:即时编译5.分布式6.动态性:反射机制7.多线程8.安全性9.健壮性二、Java三大版本1.JavaSE:标准版(桌面开发,控制台开发等)2.JavaME:嵌入式开发(......
  • Java ClickHouse整合—官方教程
    一、开发环境OpenJDK版本>=17ClickHouse:20.7+ 1、支持的数据类型FormatSupportCommentAggregatedFunction❌limitedto groupBitmap,andknowntohaveissuewith64bitbitmapArray(*)✅Bool✅Date*✅DateTime*✅Decimal*✅SEToutput_format_decimal_trailing_zeros=1 in......
  • Java学习路径
    一、Java学习路径   1.JavaSE  2.数据库   3.前端  4.JavaWeb  5.SSM框架  6.Linux  7.SpringBoot  8.SpringCloud  9.Hadoop......
  • [Javascript] Improve performance of Array.reduce
    Comparetwocodesnippetconstpeople=[{id:1,name:'John',age:45},{id:2,name:"Op",age:32},{id:3,name:"Wade",age:39}]//option1constres=people.reduce((acc,curr)=>{return({......
  • JAVA - 面向对象编程
    面向对象是在之前基本实现的基础上的又一抽象,这里的“高内聚,低耦合”体现的更加明显,有抽丝剥茧、不断总结的感觉了。学习的时候真的会有拨开云雾见月明的感觉,这种感觉真的会上瘾。你慢慢发现所有的东西都在以自己的规律运转,发现验证使用,真的很有《实践论》的味道。我能做些......
  • java -- Math、BigInteger、BigDecimal类和基本类型的包装类、正则表达式
    Mathjava.lang.Math类包含用于执行基本数学运算的方法,如初等指数、对数、平方根和三角函数。类似这样的工具类,其所有方法均为静态方法,并且不会创建对象,调用起来非常简单。Math.PI//静态常量publicstaticfinaldoublePI=3.14159265358979323846;abs//返回参数的绝对......
  • Java多版本切换 8-11-17
    Java版本切换在A:\DevEnvironment\javaVersion目录下,创建Windows命令脚本Java8.bat @echooff setJAVA_HOME=A:\DevEnvironment\jdk-1.80_152 setPath=%JAVA_HOME%\bin;%Path% echoVersionhasbeenswitchedtoJava8.Java11.bat @echooff setJAVA_HOME=A:\DevE......
  • 【开源免费】ChatGPT-Java版SDK重磅更新至1.0.10版,支持Tokens计算,快来一键接入。
    简介ChatGPTJava版SDK开源地址:https://github.com/Grt1228/chatgpt-java,目前收获将近1000个star。有bug欢迎朋友们指出,互相学习,所有咨询全部免费。最新版:1.0.10<dependency><groupId>com.unfbx</groupId><artifactId>chatgpt-java</artifactId><version......
  • salesforce学习笔记(3-1)- JavaScript Promise(LWC)
    在JS代码中,Promise到底有什么作用?首先,我们知道的是,Javascript是单线程的,什么意思呢?就是说JS在同一时间只能做一个操作,代码的执行是一行一行进行的:  这种执行方式带来的问题就是在我们打开某个画面的时候,画面可能会卡住转圈、加载中状态很久,用户体验感很差。Promise可用于......
  • Jenkins Maven Java项目
     [root@localhost~]#catx2.sh#!/bin/bashrm-rfjavawebappname=$1pid=`ps-ef|grep$appname|grep'java-jar'|awk'{printf$2}'`echo$pidif[-z$pid];thenecho"$appnamenotstarted"else......