Java锁之ReentrantReadWriteLock

Wesley13
• 阅读 703

一、前言

上一篇Java锁之ReentrantLock(二)分析了ReentrantLock实现利器AQS同步器,通过AQS源码分析,我们知道了同步器通过sate状态进行锁的获取与释放,同时构造了双向FIFO双向链表进行线程节点的等待,线程节点通过waitStatus来判断自己需要挂起还是唤醒去获取锁。那么接下来我们继续分析ReentrantLock的读写锁,ReentrantReadWriteLock锁。

二、ReentrantReadWriteLock总览

ReentrantReadWriteLock锁 实际也是继承了AQS类来实现锁的功能的,上一篇Java锁之ReentrantLock(二)已经详细解析过AQS的实现,如果已经掌握了AQS的原理,相信接下来的读写锁的解析也非常容易。

  • ReentrantReadWriteLock锁内部类列表

作用

Sync,

继承AQS,锁功能的主要实现者

FairSync

继承Sync,主要实现公平锁

NofairSync

继承Sync,主要实现非公平锁

ReadLock

读锁,通过sync代理实现锁功能

WriteLock

写锁,通过sync代理实现锁功能

Java锁之ReentrantReadWriteLock

我们先分析读写锁中的这4个int 常量,其实这4个常量的作用就是区分一个int整数的高16位和低16位的,ReentrantReadWriteLock锁还是依托于state变量作为获取锁的标准,那么一个state变量如何区分读锁和写锁呢?答案是通过位运算,高16位表示读锁,低16位表示写锁。如果对位运算不太熟悉或者不了解的同学可以看看这篇文章《位运算》。既然是分析读写锁,那么我们先从读锁和写锁的源码获取入手分析。

这里先提前补充一个概念:

写锁和读锁是互斥的(这里的互斥是指线程间的互斥,当前线程可以获取到写锁又获取到读锁,但是获取到了读锁不能继续获取写锁),这是因为读写锁要保持写操作的可见性,如果允许读锁在被获取的情况下对写锁的获取,那么正在运行的其他读线程无法感知到当前写线程的操作。因此,只有等待其他线程都释放了读锁,写锁才能被当前线程获取,而一旦写锁被获取,其他读写线程的后续访问都会被阻塞。

  • 写锁tryLock()

我们根据内部类WriteLock的调用关系找到源码如下,发现最终写锁调用的是tryWriteLock()(以非阻塞获取锁方法为例)

 public boolean tryLock( ) {
            return sync.tryWriteLock();
        }
        
        
 final boolean tryWriteLock() {
            Thread current = Thread.currentThread();
            int c = getState();
            if (c != 0) {//状态不等于0,说明已经锁已经被获取过了
                int w = exclusiveCount(c);//这里是判断是否获取到了写锁,后面会详细分析这段代码
                // 这里就是判断是否是锁重入:2种情况
                // 1.c!=0说明是有锁被获取的,那么w==0,
                // 说明写锁是没有被获取,也就是说读锁被获取了,由于写锁和读锁的互斥,为了保证数据的可见性
                // 所以return false.
                //2. w!=0,写锁被获取了,但是current != getExclusiveOwnerThread() ,
                // 说明是被别的线程获取了,return false;
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                if (w == MAX_COUNT)//判断是否溢出
                    throw new Error("Maximum lock count exceeded");
            }
            // 尝试获取锁
            if (!compareAndSetState(c, c + 1))
                return false;
            setExclusiveOwnerThread(current);
            return true;
        }
  • 读锁tryLock() 同样我们先分析非阻塞获取锁方法,tryReadLock()

    final boolean tryReadLock() { Thread current = Thread.currentThread(); for (;;) { int c = getState(); if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) return false; //写锁被其他线程获取了,直接返回false int r = sharedCount(c); //获取读锁的状态 if (r == MAX_COUNT) throw new Error("Maximum lock count exceeded"); if (compareAndSetState(c, c + SHARED_UNIT)) { //尝试获取读锁 if (r == 0) { //说明第一个获取到了读锁 firstReader = current; //标记下当前线程是第一个获取的 firstReaderHoldCount = 1; //重入次数 } else if (firstReader == current) { firstReaderHoldCount++; //次数+1 } else { //cachedHoldCounter 为缓存最后一个获取锁的线程 HoldCounter rh = cachedHoldCounter; if (rh == null || rh.tid != getThreadId(current)) cachedHoldCounter = rh = readHolds.get(); //缓存最后一个获取锁的线程 else if (rh.count == 0)// 当前线程获取到了锁,但是重入次数为0,那么把当前线程存入进去 readHolds.set(rh); rh.count++; } return true; } } }

  • 读锁的释放tryReleaseShared()

写锁的释放比较简单,基本逻辑和读锁的释放是一样的,考虑到篇幅,这次主要分析读锁的释放过程:

 protected final boolean tryReleaseShared(int unused) {
            Thread current = Thread.currentThread();
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
                if (firstReaderHoldCount == 1)//如果是首次获取读锁,那么第一次获取读锁释放后就为空了
                    firstReader = null;
                else
                    firstReaderHoldCount--;
            } else {
                HoldCounter rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                int count = rh.count;
                if (count <= 1) { //表示全部释放完毕
                    readHolds.remove();  //释放完毕,那么久把保存的记录次数remove掉
                    if (count <= 0)
                        throw unmatchedUnlockException();
                }
                --rh.count;
            }
            for (;;) {
                int c = getState();
                 // nextc 是 state 高 16 位减 1 后的值
                int nextc = c - SHARED_UNIT;
                if (compareAndSetState(c, nextc)) //CAS设置状态
                    
                    return nextc == 0; //这个判断如果高 16 位减 1 后的值==0,那么就是读状态和写状态都释放了
            }
        }

上面就是读写锁的获取和释放过程源码,暂时先分析简单的非阻塞获取锁方法,根据源码我们可以知道,写锁和读锁的是否获取也是判断状态是否不为0,写锁的状态获取方法是exclusiveCount(c) ,读锁的状态获取方法是sharedCount(c) 。那么我们接下来分析下这两个方法是如何对统一个变量位运算获取各自的状态的,在分析之前我们先小结下前面的内容。

  • 小结一下

a. 读写锁依托于AQS的State变量的位运算来区分读锁和写锁,高16位表示读锁,低16位表示写锁。

b. 为了保证线程间内容的可见性,读锁和写锁是互斥的,这里的互斥是指线程间的互斥,当前线程可以获取到写锁又获取到读锁,但是获取到了读锁不能继续获取写锁。

三、Sync 同步器位运算分析

  • 状态变量按照位划分示意图

Java锁之ReentrantReadWriteLock

我们再看看位运算的相关代码(我假设你已经知道了位运算的相关基本知识,如果不具备,请阅读《位运算》

        static final int SHARED_SHIFT   = 16;
        //实际是65536
        static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
        //最大值 65535
        static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
        // 同样是65535
        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; }

我们按照图示内容的数据进行运算,图示的32位二进制数据为: 00000000000000100000000000000011

  • 读状态获取

00000000000000100000000000000011 >>> 16,无符号右移16位,结果如下: 00000000000000000000000000000010,换算成10进制数等于2,说明读状态为: 2

  • 读状态获取

00000000000000100000000000000011 & 65535,转换成2进制运算为 00000000000000100000000000000011 & 00000000000000001111111111111111

最后与运算结果为: 00000000000000100000000000000011 ,换算成10进制为3

不得不佩服作者的思想,这种设计在不修改AQS的代码前提下,仅仅通过原来的State变量就满足了读锁和写锁的分离。

四、锁降级

锁降级是指写锁降级为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(之前拥有的写锁的过程)源码示例(来自于《java并发编程的艺术》):

public void processData(){
    readLock.lock();
    if(!update){
        //必须先释放读锁
        readLock.unlock();
        //锁降级从写锁获取到开始
        writeLock.lock();
        try{
            if(!update){
                update =true;
            }
            readlock.lock();
        }finally{
            writeLock.unlock();
        }//锁降级完成,写锁降级为读锁
    }
    try{
        //略
    }finally{
        readLock.unlock();
    }
}

上述示例就是一个锁降级的过程,需要注意的是update变量是一个volatie修饰的变量,所以,线程之间是可见的。该代码就是获取到写锁后修改变量,然后获取读锁,获取成功后释放写锁,完成了锁的降级。注意:ReentrantReadWriteLock不支持锁升级,这是因为如果多个线程获取到了读锁,其中任何一个线程获取到了写锁,修改了数据,其他的线程感知不到数据的更新,这样就无法保证数据的可见性。

最后总结

  • 源码中,涉及了其他部分,本文做了精简,比如:cachedHoldCounter,firstReader firstReaderHoldCount等属性,这些属性并没有对理解原理有多少影响,主要是提升性能的作用,所以本文没有讨论。
  • 读写锁还是依赖于AQS的自定义同步器来实现的,里面的大部分代码和之前分析的两篇文章《Java锁之ReentrantLock》差不多,AQS的大部分解析已经在这两篇文章已经解析过了,如果读者对此还有疑惑的地方,可以看看这两篇文章。
  • 读写锁的巧妙设计,就是对AQS的锁状态进行为运算,区分了读状态和写状态。
点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Wesley13 Wesley13
3年前
Java爬虫之JSoup使用教程
title:Java爬虫之JSoup使用教程date:201812248:00:000800update:201812248:00:000800author:mecover:https://imgblog.csdnimg.cn/20181224144920712(https://www.oschin
Wesley13 Wesley13
3年前
P2P技术揭秘.P2P网络技术原理与典型系统开发
Modular.Java(2009.06)\.Craig.Walls.文字版.pdf:http://www.t00y.com/file/59501950(https://www.oschina.net/action/GoToLink?urlhttp%3A%2F%2Fwww.t00y.com%2Ffile%2F59501950)\More.E
Stella981 Stella981
3年前
Android So动态加载 优雅实现与原理分析
背景:漫品Android客户端集成适配转换功能(基于目标识别(So库35M)和人脸识别库(5M)),导致apk体积50M左右,为优化客户端体验,决定实现So文件动态加载.!(https://oscimg.oschina.net/oscnet/00d1ff90e4b34869664fef59e3ec3fdd20b.png)点击上方“蓝字”关注我
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Wesley13 Wesley13
3年前
35岁是技术人的天花板吗?
35岁是技术人的天花板吗?我非常不认同“35岁现象”,人类没有那么脆弱,人类的智力不会说是35岁之后就停止发展,更不是说35岁之后就没有机会了。马云35岁还在教书,任正非35岁还在工厂上班。为什么技术人员到35岁就应该退役了呢?所以35岁根本就不是一个问题,我今年已经37岁了,我发现我才刚刚找到自己的节奏,刚刚上路。
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这