Java读写锁ReadWriteLock原理与应用场景详解
Java并发编程提供了读写锁,主要用于读多写少的场景,今天我就重点来讲解读写锁的底层实现原理@mikechen 目录
什么是读写锁?
读写锁并不是JAVA所特有的读写锁(Readers-Writer Lock)顾名思义是一把锁分为两部分:读锁和写锁,其中读锁允许多个线程同时获得,因为读操作本身是线程安全的,而写锁则是互斥锁,不允许多个线程同时获得写锁,并且写操作和读操作也是互斥的。
所谓的读写锁(Readers-Writer Lock),顾名思义就是将一个锁拆分为读锁和写锁两个锁。
其中读锁允许多个线程同时获得,而写锁则是互斥锁,不允许多个线程同时获得写锁,并且写操作和读操作也是互斥的。
为什么需要读写锁?
Synchronized 和 ReentrantLock 都是独占锁,即在同一时刻只有一个线程获取到锁。
然而在有些业务场景中,我们大多在读取数据,很少写入数据,这种情况下,如果仍使用独占锁,效率将及其低下。
针对这种情况,Java提供了读写锁——ReentrantReadWriteLock。
主要解决:对共享资源有读和写的操作,且写操作没有读操作那么频繁的场景。 读写锁的特点公平性:读写锁支持非公平和公平的锁获取方式,非公平锁的吞吐量优于公平锁的吞吐量,默认构造的是非公平锁 可重入:在线程获取读锁之后能够再次获取读锁,但是不能获取写锁,而线程在获取写锁之后能够再次获取写锁,同时也能获取读锁 锁降级:线程获取写锁之后获取读锁,再释放写锁,这样实现了写锁变为读锁,也叫锁降级 读写锁的使用场景
ReentrantReadWriteLock适合读多写少的场景:
读锁ReentrantReadWriteLock.ReadLock可以被多个线程同时持有, 所以并发能力很高。
写锁ReentrantReadWriteLock.WriteLock是独占锁, 在一个线程持有写锁时候, 其他线程都不能在抢占, 包含抢占读锁都会阻塞。
ReentrantReadWriteLock的使用场景总结:其实就是 读读并发、读写互斥、写写互斥而已,如果一个对象并发读的场景大于并发写的场景,那就可以使用 ReentrantReadWriteLock来达到保证线程安全的前提下提高并发效率。 读写锁的主要成员和结构图
1. 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();}
读写锁 ReadWriteLock
读写锁维护了一对相关的锁,一个用于只读操作,一个用于写入操作。
只要没有写入,读取锁可以由多个读线程同时保持,写入锁是独占的。
2.ReentrantReadWriteLock的核心变量
ReentrantReadWriteLock类包含三个核心变量: ReaderLock:读锁,实现了Lock接口 WriterLock:写锁,也实现了Lock接口 Sync:继承自AbstractQueuedSynchronize(AQS),可以为公平锁FairSync 或 非公平锁NonfairSync
3.ReentrantReadWriteLock的成员变量和构造函数 /** 内部提供的读锁 */ private final ReentrantReadWriteLock.ReadLock readerLock; /** 内部提供的写锁 */ private final ReentrantReadWriteLock.WriteLock writerLock; /** AQS来实现的同步器 */ final Sync sync; /** * Creates a new {@code ReentrantReadWriteLock} with * 默认创建非公平的读写锁 */ public ReentrantReadWriteLock() { this(false); } /** * Creates a new {@code ReentrantReadWriteLock} with * the given fairness policy. * * @param fair {@code true} if this lock should use a fair ordering policy */ public ReentrantReadWriteLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); readerLock = new ReadLock(this); writerLock = new WriteLock(this); }读写锁的实现原理
ReentrantReadWriteLock实现关键点,主要包括: 读写状态的设计 写锁的获取与释放 读锁的获取与释放 锁降级
1.读写状态的设计
之前谈ReentrantLock的时候,Sync类是继承于AQS,主要以int state为线程锁状态,0表示没有被线程占用,1表示已经有线程占用。
同样ReentrantReadWriteLock也是继承于AQS来实现同步,那int state怎样同时来区分读锁和写锁的?
如果在一个整型变量上维护多种状态,就一定需要"按位切割使用"这个变量,ReentrantReadWriteLock将int类型的state将变量切割成两部分: 高16位记录读锁状态 低16位记录写锁状态
abstract static class Sync extends AbstractQueuedSynchronizer { // 版本序列号 private static final long serialVersionUID = 6317671515068378041L; // 高16位为读锁,低16位为写锁 static final int SHARED_SHIFT = 16; // 读锁单位 static final int SHARED_UNIT = (1 << SHARED_SHIFT); // 读锁最大数量 static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1; // 写锁最大数量 static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; // 本地线程计数器 private transient ThreadLocalHoldCounter readHolds; // 缓存的计数器 private transient HoldCounter cachedHoldCounter; // 第一个读线程 private transient Thread firstReader = null; // 第一个读线程的计数 private transient int firstReaderHoldCount;}
2.写锁的获取与释放 protected final boolean tryAcquire(int acquires) { /* * Walkthrough: * 1. If read count nonzero or write count nonzero * and owner is a different thread, fail. * 2. If count would saturate, fail. (This can only * happen if count is already nonzero.) * 3. Otherwise, this thread is eligible for lock if * it is either a reentrant acquire or * queue policy allows it. If so, update state * and set owner. */ Thread current = Thread.currentThread(); int c = getState(); //获取独占锁(写锁)的被获取的数量 int w = exclusiveCount(c); if (c != 0) { // (Note: if c != 0 and w == 0 then shared count != 0) //1.如果同步状态不为0,且写状态为0,则表示当前同步状态被读锁获取 //2.或者当前拥有写锁的线程不是当前线程 if (w == 0 || current != getExclusiveOwnerThread()) return false; if (w + exclusiveCount(acquires) > MAX_COUNT) throw new Error("Maximum lock count exceeded"); // Reentrant acquire setState(c + acquires); return true; } if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) return false; setExclusiveOwnerThread(current); return true; }
1)c是获取当前锁状态,w是获取写锁的状态。
2)如果锁状态不为零,而写锁的状态为0,则表示读锁状态不为0,所以当前线程不能获取写锁。或者锁状态不为零,而写锁的状态也不为0,但是获取写锁的线程不是当前线程,则当前线程不能获取写锁。
3)写锁是一个可重入的排它锁,在获取同步状态时,增加了一个读锁是否存在的判断。
写锁的释放与ReentrantLock的释放过程类似,每次释放将写状态减1,直到写状态为0时,才表示该写锁被释放了。
3.读锁的获取与释放 protected final int tryAcquireShared(int unused) { for(;;) { int c = getState(); int nextc = c + (1<<16); if(nextc < c) { throw new Error("Maxumum lock count exceeded"); } if(exclusiveCount(c)!=0 && owner != Thread.currentThread()) return -1; if(compareAndSetState(c,nextc)) return 1; }}
1)读锁是一个支持重进入的共享锁,可以被多个线程同时获取。
2)在没有写状态为0时,读锁总会被成功获取,而所做的也只是增加读状态(线程安全)
3)读状态是所有线程获取读锁次数的总和,而每个线程各自获取读锁的次数只能选择保存在ThreadLocal中,由线程自身维护。
读锁的每次释放均减小状态(线程安全的,可能有多个读线程同时释放锁),减小的值是1<<16。
4.锁降级
降级是指当前把持住写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。
锁降级过程中的读锁的获取是否有必要,答案是必要的。主要是为了保证数据的可见性,如果当前线程不获取读锁而直接释放写锁,假设此刻另一个线程获取的写锁,并修改了数据,那么当前线程就步伐感知到线程T的数据更新,如果当前线程遵循锁降级的步骤,那么线程T将会被阻塞,直到当前线程使数据并释放读锁之后,线程T才能获取写锁进行数据更新。
5.读锁与写锁的整体流程
读写锁总结
本篇详细介绍了ReentrantReadWriteLock的特征、实现、锁的获取过程,通过4个关键点的核心设计: 读写状态的设计 写锁的获取与释放 读锁的获取与释放 锁降级
从而才能实现:共享资源有读和写的操作,且写操作没有读操作那么频繁的应用场景。
以上
架构技术合集分布式架构设计从0到1全部合集(附:分布式、微服务、高并发等大型网站架构)JVM(Java虚拟机)从0到1全部合集(建议收藏)Java多线程与并发从0到1全部合集(面试必看)Redis分布式缓存从0到1全部合集(进阶必看)Spring开发框架从0到1全部合集(建议收藏) MySQL数据库从0到1全部合集(建议收藏)
郝蕾越来越富态了,穿紧身黑裙掩不住小肚腩,珠圆玉润也挺美从今天起记录我的2023年轻时候的郝蕾,也是典型的文艺女神。气质出众,长相甜美,透着文艺女神的忧郁感,总是让人莫名的喜欢。随着年龄的增长,郝蕾越来越富态了。本就有些偏大的骨架,再加
国产化妆品企业,强者恒强上周研究完医美行业有些意犹未尽,虽说化妆品企业和医美企业之间差距较大,但因为它们都和大家的变美需求相关,所以市场表现有很大的联动性,二者同属于美容护理行业。所以今天我们就再来看看化
波司登今年的羽绒服杀疯了吧,也太美了,除了贵点没啥缺点!这两天突然降温了,在寒冷的冬天,没有什么比一件羽绒服更能抵御寒冷的了。羽绒服在保暖上拥有其他衣服无法取代的地位。因为它既保暖又轻便,穿着还舒适,即便是胖胖肥肥的面包服看起来也不臃肿
今年穿长大衣裙子精致时髦精,美极了!嗨,各位小仙女们,大家好呀!还有几天就是新年了,姐妹们,想好在过年的时候怎么穿了吗?想要既美丽动人,又暖和一些?那么不妨试试长大衣裙子吧,这样的穿搭真的很精致时髦,美极了!接下来一
一切顺其自然,老天自有安排当我们遇到迈不过去的坎时,与其烦恼痛苦,不如顺其自然,或许就会柳暗花明又一村。只有凡事看淡,才会感受到生活的乐趣,找到活着的意义。在烦心时,抄抄心经,写写文字,内心也会舒服许多。刚
人间随想人间随想(苏子龙)冬着棉衣夏着衫,春秋两季随意穿。来时哭泣不争眼,走后无声化作烟。粉墨登场成人后,喜怒哀乐数十年。养儿育女费心机,吃喝拉撒忙不闲。你能我胜争优劣,我富你贫分暖寒。荣
灯火璀璨,热闹迎春!佛山古镇灯会正式亮灯火树银花盛景,红梅绿柳新春。1月17日晚,2023最岭南之佛山过大年第十三届佛山(禅城)岭南年俗欢乐节开幕式暨佛山古镇灯会启动仪式在佛山岭南天地举行。现场,福兔迎春意浓情精彩百业聚
免费!优惠!嘉兴一大波旅游福利来袭!来源嘉兴日报嘉兴在线免费!优惠!记者今天从嘉兴市文旅局了解到,嘉兴即将开启2023一起来嘉游缤纷迎春汇文旅惠民消费季活动,为在嘉来嘉的你奉上精彩纷呈的文化旅游大餐。大餐包含了嘉兴各
新春路京张高铁迎冬奥后首个滑雪季本趟列车由清河站始发,经停八达岭长城站下花园北站以及太子城站,最终到达崇礼站,全程1小时28分钟。春节前的最后一个周末,北京清河站人来人往,携带着各式各样雪具的游客们正兴冲冲地搭乘
观海二路的楼梯!老青岛的道路三大怪之一,转了一圈又回来,指的就是观海二路。这是一条神奇的道路,已经有很多人讲过了它的特点,但是今天我要讲讲观海二路上的楼梯。观海山的俯瞰图观海二路是一条环绕观海山的
下一站,金坛茅山!我在这里等你回家过年!随着春节的临近,紧张的工作和忙碌的节奏,都被心底涌动的思念所占据,今年你是否回到久别的家乡?看过世界的人,最想回家。在城市的工作生活中,逐渐迷失的我们,无论是为了疗伤还是净化,都迫
摊牌了!广东对位张镇麟之人曝光,杜锋全力筹备辽粤大战首先先恭喜广东男篮,以12396的比分大胜了北控男篮。这场比赛估计是广东队赢得最轻松的一场比赛,多名主力球员早早就打卡下班,下半场时间基本上都在练兵。北控队没有放弃比赛,一直都是换
躺平亦能大起写在2022最后一天今天是2022年最后一天,回首这一年,似乎只有苦涩,这两年经历了人生至暗时刻,从希望到失望,再到绝望,又重新在绝望中寻找希望,最后再到失望绝望,如此这般,循环往复。多少次以为自己熬
2022年过去了,我一点也不想念它!一2022年要过去了,我们并不怀念它!2022年来临的时候,我们满怀期待,2022接近尾声的时候,内心却是五味杂陈!这一年我们经历太多的不可思议,目睹了一场场编剧也写不出的荒唐!如
泰晤士报D福法纳的母队将莫尔德起诉至体育仲裁法庭直播吧1月13日讯据泰晤士报报道,切尔西新援大卫福法纳的母队阿比让城将莫尔德起诉至体育仲裁法庭。来自于科特迪瓦的福法纳本月加盟了切尔西,转会费为1300万欧元。并且他在球队同曼城的
乔科尔菲利克斯的红牌改变了比赛切尔西需要将信心带回来直播吧1月13日讯在今日凌晨进行的英超联赛第7轮的一场补赛中,切尔西12不敌富勒姆,菲利克斯蓝军首秀染红。在BTSport的节目中,前切尔西中场乔科尔在谈到这一点时表示波特必须思考
聊一聊半程评奖过去几个赛季,每年这个时候都会安排一个半程评奖系列。去年没弄,也忘了为啥没弄,可能是嫌这个系列码字太多,搞得太累。现在心态有了一些变化,明白写太多意义不大,我累,你们也累。今年的半
完败!亚历山大378,恩比德17中10,哈登创纪录,成历史第一人北京时间1月13日,NBA常规赛76人主场迎战雷霆,赛前,76人25胜15负排名东部第4,而雷霆18胜23负位居西部第13,哈登和恩比德会有怎样的发挥值得期待。首发阵容方面,76人
退役10年后,游泳名将吴鹏归来为亚运而战?2013年全运会男子4100米混合泳接力,吴鹏所在的浙江队夺冠。资料图视觉中国北京时间1月12日晚,美国游泳系列赛诺克斯维尔站男子100米蝶泳预赛鸣枪。最受外界关注的是预赛第2组的
这样和你聊天的人,就别聊了,以免坏了自己的心情,打扰了别人这样和你聊天的人,就别聊了,以免打扰了别人,坏了自己的心情感情里,最无奈的,不是没遇到自己喜欢的人,而是遇到了,自己想方设法对他好,绞尽脑汁去找话题跟他聊天,毫无底线对他迁就,却换
烟花的随想吃过晚饭坐在沙发上随意的翻看着手机,忽然屋外传来隆隆的鞭炮声,顺着声音传来的方向,一个箭步跑到窗台边和儿子欣赏着不远处燃放着的烟花,一个个火球冲向夜空,划破了宁静的夜,即使城市里灯
很多人其实并不知道和了解未来的趋势与自己的优势很多人其实并不知道和了解未来的趋势和自己的优势。有时候,有心栽花花不开,无心插柳却是柳成荫。忙死忙活所做的事情未必成功,因为比我们有实力有背景有关系的人太多太多,我们虽然干了许多年