Java中ReentrantReadWriteLock读写锁的实现
目录
- 一、锁的分类
- 二、ReentrantReadwriteLock的基本操作
- 三、ReentrantReadWriteLock的底层实现
- 四、ReentrantReadWriteLock的锁重入
- 五、ReentrantReadWriteLock的写锁饥饿
- 六、ReentrantReadWriteLock的锁降级
- 七、ReentrantReadWriteLock的优化
一、锁的分类
这里不会对Java中大部分的分类都聊清楚,主要把 **互斥,共享** 这种分类聊清楚。
Java中的互斥锁,synchronized,ReentrantLock这种都是互斥锁。一个线程持有锁操作时,其他线程都需要等待前面的线程释放锁资源,才能重新尝试竞争这把锁。
Java中的读写锁(支撑互斥&共享),Java中最常见的就是 **ReentrantReadWriteLock** ,StampedLock。
其中StampedLock是JDK1.8中推出的一款读写锁的实现,针对ReentrantReadWriteLock一个优化。但是,今儿不细聊。主要玩ReentrantReadWriteLock。
ReentrantReadWriteLock主要就是解决咱们刚才聊的,读写操作都有,读操作居多,写操作频次相对比较低的情况,可以使用读写锁来提升系统性能。
读写锁中:
* 写写互斥
* 读写互斥* 写读互斥* 读读共享* 有锁降级的情况,后面聊!!二、ReentrantReadWriteLock的基本操作
ReentrantReadWriteLock中实现了ReadWriteLock的接口,在这个接口里面提供了两个抽象方法。
正常的操作,是new ReentrantReadWriteLock的对象,但是你具体的业务操作是需要读锁,还是写锁,你需要单独的获取到,然后针对性的加锁。
public interface ReadWriteLock { /** * Returns the lock used for reading. * * @return the lock used for reading */ Lock readLock(); /** * Returns the lock used for writing. * * @return the lock used for writing */ Lock writeLock(); }
具体使用方式
pwww.devze.comublic static void main(String[] args){ // 1、构建读写锁对象 ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); // 2、单独获取读、写锁对象 ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock(); ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock(); // 3、根据业务使用具体的锁对象加锁 writeLock.lock(); // try-finally的目的,是为了避免没有及时释放锁资源导致死锁的问题。 try{ // 4、业务操作………… System.out.println("写操作"); }finally { // 5、释放锁 writeLock.unlock(); } }
三、ReentrantReadWriteLock的底层实现
ReentrantReadWriteLock是基于AQS实现的。
AQS是JUC包下的一个抽象类AbstractQueuedSynchronizer
暂时只关注两点,分别是AQS提供的state属性,还有AQS提供的一个同步队列。
state属性,用来标识当前 读写锁 的资源是否被占用的核心标识。
private volatile int state;一个int类型的state,是4字节,每个字节占用8个bit位,一个state占用32个bit位。
* 高16位,作为读锁的标记。
* 低16位,作为写锁的标记。static final int SHARED_SHIFT = 16; static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; 00000000 00000000 11111111 11111111 /** 查看读锁的占用情况。 */ static int sharedCount(int state) { return state >>> SHARED_SHIFT; } /** Returns the number of exclusive holds represented in count */ static int exclusiveCount(int state) { return state & EXCLUSIVE_MASK; } 00000000 00000000 00000000 00000000 int类型的数值的32个bit位。 读锁占用情况: 00000000 00000011 00000000 00000000 state >>> 16 00000000 00000000 00000000 00000011 读锁被获取了三次。 写锁占用情况。(这里之所以&这个么东西,是对后期的锁降级有影响~) 00000000 00000000 00000000 00000001 state & 00000000 00000000 11111111 11111111 = 00000000 00000000 00000000 00000001 写锁被获取了一次。
一个同步队列,当线程获取锁资源失败时,需要到这个同步队列中排队。到了合适的时机,就会继续尝试获取对应的锁资源。
四、ReentrantReadWriteLock的锁重入
同一个线程,多次获取同一把锁时,就会出现锁重入的情况。
而咱们大多数的锁,都会提供锁重入的功能。
锁重入场景:
public class Demo { // 1http://www.devze.com、构建读写锁对象 static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); // 2、单独获取读、写锁对象 static ReentrantReadWriteLock.ReadLock readLock; static ReentrantReadWriteLock.WriteLock writeLock; static{ // 2、单独获取读、写锁对象 readLock = readWriteLock.readLock(); writeLock = readWriteLock.writeLock(); } public static void maiphpn(String[] args){ // 3、根据业务使用具体的锁对象加锁 writeLock.lock(); // try-finally的目的,是为了避免没有及时释放锁资源导致死锁的问题。 try{ // 4、业务操作…………调用其他方法 xxx(); }finally { // 5、释放锁 writeLock.unlock(); } } private static void xxx(){ writeLock.lock(); try{ // 其他按业务 }finally { writeLock.unlock(); } } }
咱们底层的锁重入逻辑很简单
**写锁:** 写锁的实现就是每一次获取写锁时,会对state的低16位+1,再次获取,再次+1。同理,每次释放锁资源时,也需要对state进行-1。 而当对state的低16位减到0时,锁资源就释放干净了。
读锁: 首先,读锁是共享的,他用state的高16位来维护信息。如果高16位的state的值,经过运算,知道了是4,也就是读锁被获取了4次。可能A线程获取了2次读锁资源。 B线程获取了2次读锁资源。高位的state自然就是4。但是因为程序员写代码除了问题,使用A线程,释放了4次读锁资源,那此时B线程是不是就可能出现数据安全问题了。
所以,为了解决上述的问题,每个线程需要独立的记录自己获取了几次读锁资源。可以使用ThreadLocal来保存线程局部的信息,每次加锁时,ThreadLocal中需要存储一个标记,每次+1。每次释放锁时,也需要将ThreadLocal中的标记进行-1。读线程最后是基于自己的ThreadLocal中的数值,来确认读锁是否释放干净。
五、ReentrantReadWriteLock的写锁饥饿
写锁饥饿的问题。
如果写线程在AQS中排队,并且排在head.next的位置。 那么其他想获取读锁的读线程需要排队。避免大量的读请求获取读锁,让写线程一直AQS队列中排队,无法执行写操作的问题。
通过源码可以看到,读写锁中,仅仅针对head.next这个节点的情况,来确认读线程获取读锁时是否需要排队
// 这个方法,总结一句话。 python // AQS中有排队的Node,并且head的next节点是一个有线程并且在等待写锁的Node final boolean apparentlyFirstQueuedIsExclusive() { Node h, s; return (h = head) != null && (s = h.next) != null && !s.isShared() && s.thread != null; }
ReentrantReadWriteLock读写锁中有锁降级,但是这个和synchronized的锁升级没任何关系!!!
六、ReentrantReadWriteLock的锁降级
ReentrantReadWriteLock的锁降级是指当前线程如果持有了写锁,可以降级直接获取到读锁。
在读写锁中,持有写锁的同时,再去获取读锁,这种行为一般被称为 **锁降级** php。
在读写锁中,持有读锁的同时,去获取写锁,这种行为被称为 **锁升级** ,这个行为是不允许的。
这里是获取读锁的的逻辑,看一下锁降级的支持方式
// 竞争读锁。 if (exclusiveCount(c) != 0 && // 这行代表某个线程持有写锁 getExclusiveOwnerThread() != current) // 这行代表持有写锁的不是当前线程 // 退出竞争,无法获取读锁 return -1;
前面逻辑没有走return - 1之后,在后续就会正常的对state的高位+1,并且完成读锁的计数操作。
七、ReentrantReadWriteLock的优化
ReentrantReadWriteLock的优化主要是在读锁计数层面上做的优化。
这个对性能的优化微乎其微,但是确确实实是一个优化。
在获取读锁时,因为是共享的,这种优化只针对第一个获取读锁的线程和最后一个获取读锁的线程。
针对第一个获取读锁的线程,他采用一个全局变量记录重入次数。这个操作可以节省掉使用ThreadLocal的时间成本和内存成本。
其中firstReader记录第一个获取读锁的线程。
firstReaderHoldCount,记录第一个获取读锁的线程的重入次数。
这里是最后一个获取读锁的线程需要走的逻辑
cachedHoldCounter这个属性是记录最后一个获取读锁的线程的重入次数。
这里可以让最后一个获取读锁的线程在重入时,省略掉去ThreadLocal中get计数器的操作,但是之前的set存储操作,不能省略
// 获取上次最后获取读锁的线程 HoldCounter rh = cachedHoldCounter; // 查看当前线程是否是之前的cachedHoldCounter if (rh == null || rh.tid != getThreadId(current)) // 说明不是,将当前获取读锁的线程设置为cachedHoldCounter cachedHoldCounter = rh = readHolds.get(); // 这个判断代表第一次获取读锁才会进去 else if (rh.count == 0) // 如果是第一次获取读锁,不是重入,还是需要扔到ThreadLocal里纪录好,。 readHolds.set(rh); // 直接对获取到的rh做++操作,代表获取了一次读锁。 rh.count++;
到此这篇关于Java中ReentrantReadWriteLock读写锁的实现的文章就介绍到这了,更多相关ReentrantReadWriteLock读写锁内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!
精彩评论