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