在多线程环境下,为了保证线程安全, 我们通常会对共享资源加锁操作,我们常用Synchronized关键字或者ReentrantLock 来实现,这两者加锁方式都是排他锁,即同一时刻最多允许一个线程操作,然而大多数场景中对共享资源读多于写,那么存在线程安全问题的是写操作(修改,添加,删除),我们是否应该考虑将读和写两个分开,只要运用合理,并发性能是不是可以提高,吞吐量增大呢? ReentrantReadWriteLock已经为我们实现了这种机制,我们一起来看它是怎样实现的吧!
1、读写锁的一些概念在查看可重入读写锁的源码前,有几个概念需要先知道,对于后面理解源码很有帮助。
1、ReentrantReadWriteLock 内部 Sync类依然是继承AQS实现的,因此同步状态字段 state,依然表示对锁资源的占用情况。那么如何实现一个 int类型的state 同时来表示读写锁两种状态的占用情况呢? 这里实现非常巧妙,将4个字节的int类型, 32位拆分为2部分,高16位表示读锁的占用情况,低16位表示写锁的占用情况,这样读写锁互不影响,相互独立;也因此读写锁的最大值是2^16-1 = 65535,不能超过16位,下面源码有体现。
state值表示如图所示:
2、读锁是共享锁,只要不超过最大值,可多个线程同时获取; 写锁是排他锁,同一时刻最多允许一个线程获取。
写锁与其他锁都互斥,含写写互斥,写读互斥,读写互斥。
3、state可同时表示读写锁的状态,state的高16位表示获取读锁的线程数,读锁支持可重入,即一个线程也可多次获取读锁,怎么维护每个读锁线程的重入次数的? 每个线程有一个计数器 HoldCounter,用ThreadLocal来存放每个线程的计数器;state的低16位表示写锁的同步状态,因为写锁是排他锁,这里就不能表示获取写锁的线程数了,只能表示写锁的重入次数,获取写锁的线程可多次重复获取写锁(支持重入)。
读锁的计数器的实现原理如下:
可见ThreadLocalHoldCounter继承 ThreadLocal,每个获取读锁的线程是通过其本地变量来存储自己的计数器,来统计获取读锁的重入次数。ThreadLocal原理解析
static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> { //重写了ThreadLocal的initialValue方法 public HoldCounter initialValue() { return new HoldCounter(); } }4、state的高16位需要记录获取读锁的线程数,每增加一个线程获取读锁,在state的高16执行加1操作,即state+2^16,写锁增加重入次数,直接 state+1即可。
5、锁降级:获取写锁的线程,可以再次获取到读锁,即写锁降级为读锁。
读锁可以升级为写锁吗? 不可以,因为存在线程安全问题,试想获取读锁的线程有多个,其中一个线程升级为写锁,对临界区资源进行操作,比如修改了某个值,对其他已经获取读锁的线程不可见,出现线程安全问题。
代码演示:
1、读写状态
AQS(AbstractQueuedSynchronizer的简称)中同步状态字段 private volatile int state, int类型,4个字节,32位,拆分为高16位表示读状态,低16位表示写状态,如下定义了一些常量,实现获取读写锁的数量。
ReentrantReadWriteLock部分代码如下:
//分隔位数,16位 static final int SHARED_SHIFT = 16; //读锁加1的数量,1左位移16位, (16)0x10000 = (2)1000000000000000= (10) 65536 static final int SHARED_UNIT = (1 << SHARED_SHIFT); //读写锁的最大数量, (16)0xFFFFFFFF =(2)1111111111111111 =(10)65535 static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1; //写锁的掩码,用于计算写锁重入次数时,将state的高16全部置为0, 等于(2)1111111111111111 static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; //获取读锁数,表示当前有多少个线程获取到读锁 static int sharedCount(int c) { return c >>> SHARED_SHIFT; } //获取写锁重入次数(不等于0表示有线程持有独占锁,大于1,表示写锁有重入) static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }分别看一下获取读写锁数量的方法。
获取占用读锁的线程数,代码如下:
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }传入的c为 state,state 无符号右移16位,抹去低16位值,左边补0
示例图如下:
获取写锁的值的方法
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }与运算,将高16全部置为0,低16值代表写锁的值,&运算,相同为1,不同为0,得到低16位写锁值。
示例图如下:
2、三个锁概念
int c =getState() ,获取state的值,代表同步锁状态,该值包含读写两个锁的同步状态
int w = exclusiveCount(c); w代表写锁的同步状态,通过c获取到写锁的状态值
int r = sharedCount(c); r 代表读锁的同步状态,通过c获取到读锁的状态值
以下分析三种情况下state,r, w 的值及代表的含义:
1、一个线程获取到写锁:
state =1, w =1, r =0