Reentrantreadwritelock应用和原理

目录

一、介绍

二、应用

三、原理


一、介绍

读操作远远高于写操作时,这时候使用读写锁让读-读可以并发,提高性能

类似于数据库中的共享锁 select...from...lock in share mode  

提供一个数据容器类内部分别使用读锁保护数据的read()方法,写锁保护数据的write()方法

两个线程读读不是互斥的,读写和写写是互斥的

注意事项:

  • 读锁不支持条件变量
  • 重入时升级不支持:持有读锁的情况获取写锁,会导致获取写锁永久等待,必须要先释放读锁才能获取写锁
  • 重入时降级支持:持有写锁可以获取读锁

二、应用

我们更新缓存有两种操作

先删除缓存:我们在实现缓存和数据库的时候就要考虑这种问题,比如现在有a线程去查询数据库然后准备把数据库的数据更新到缓存的时候,b线程把数据库的时候就改了,这个时候如果更新成功缓存就和数据库已经改了的数据不一致的,然后后面就一直拿缓存中的旧数据。

先更新数据库:先更新数据库的话好一点,缓存数据库不一致的情况会少一些,但是要保证强一致性,还是要加锁。因为先改库还没来得及删除缓存的时候,这个时候去查询都是旧数据,不一致的

这里加锁是可以用读写锁来优化的

  • 读锁:如果缓存中有数据,查询缓存,这里要记得释放读锁(因为如果缓存没有要查询需要写锁,持有读锁拿不了写锁)
  • 写锁:缓存中没有数据,从数据库中查询,写入缓存(如果很多个线程去抢锁可能会进队列很多个,但是这个只需要一个,所以要加双重检查)
  • 写锁:更新,先更新数据库然后删除缓存

这种优化主要体现在了读多写少的场景中读读可以共享的情况,但是如果还想更加提高并发,可以更细粒度划分。而且这种情况只适合单机场景

三、原理

读写锁用的是同一个sycn同步器因此等待队列、state等也是同一个

但是也有区别,他的state变了,因为要记录读锁和写锁两个锁的状态,高16位记录读锁,低16位记录写锁。其他流程都跟reentrantlock差不多

加写锁

加写锁和之前没有什么区别,先去检查state状态,如果状态等于0说明读和写锁都没有加,就再判断一个方法(非公平直接返回false,公平锁就去检查队列是否有,有就得排队进去,没有也返回),然后cas来加锁,成功就设置当前持锁线程到exclusiveOwnerThread。如果前面判断state不为0,那么可能读锁加了还是写锁加了,如果加了读锁,就互斥了直接return false,如果加的是写锁,看是不是自己加的,自己加的就是重入写锁状态+1,如果是别人的也return false,当然如果重入次数超过一定的次数65535也会抛出异常

加读锁

首先判断状态,然后看写锁的部分是不是0,如果不是0&&加写锁的是不是他自己,如果不是自己加的写锁,就返回-1(写锁是可以升级加读锁的,但是读锁不能加写锁)。如果为-1他就会再判断一遍能不能拿到读锁,还是不行就循环进入堵塞队列堵塞等待。

注意:

他们堵塞的线程虽然都在一个堵塞队列里,但是他们因为申请的锁不同他们的状态是不同的,等待读锁的是shared状态,等待写锁的是ex状态,然后前面的节点状态都是-1有责任区唤醒后面的节点,但是最后一个节点的状态是0,后面没他需要唤醒的

写锁释放

直接把状态数量-1,减一后查看是不是0,如果不是0就是锁重入减一了,返回false;如果减小后是0了,就返回true。返回真的话就把当前持锁线程设为null,然后就去检查堵塞队列的头节点,如果有气切状态不等于0,就唤醒他。唤醒了就继续循环去竞争锁,发现没人拿锁,就cas加写锁,高位加锁,然后return 1表示加锁成功了。然后就要取堵塞队列里面唤醒,如果是读锁,就唤醒。

换完阻塞队列的节点之后,拿到当前节点的下一个,如果下个节点是想要加读锁的shared,就把会当前节点的-1变为0,然后对后面的节点唤醒,然后让读锁的状态+1(多个线程都读锁可以让计数增加)然后把这个节点在堵塞队列删掉改为下一个,然后继续判断下一个是不是读节点,如果是重复。

读锁释放

拿到state状态,然后把读锁状态-1,然后用cas去设置state,看看能不能成功,成功后判断是不是0,如果是true,不是0就是false。如果是0就去看堵塞队列的头结点,如果头结点的状态是-1就唤醒那个节点,不是-1就重试。

猜你喜欢

转载自blog.csdn.net/weixin_54232666/article/details/131157121