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

发布时间 2023-04-07 22:09:23作者: 酷酷-

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读写锁内部的属性以及结构和读写锁的个数表示看了,下节我们就具体来看看读写锁的使用和原理分析,有理解不对的地方欢迎指正哈。