ReentrantReadWriteLock

读写锁

独占锁X:指该锁一次只能被一个线程所持有,对 ReentrantLock 和 Synchronized 而言都是独占锁

共享锁S:指该锁可以被多个线程锁持有

ReentrantReadWriteLock 其读锁是共享锁,写锁是独占锁

作用:多个线程同时读一个资源类没有任何问题,为了满足并发量,读取共享资源应该同时进行,但是如果一个线程想去写共享资源,就不应该再有其它线程可以对该资源进行读或写

使用规则:

  • 加锁解锁格式:

    复制代码
     r.lock();
     try {
         // 临界区
     } finally {
         r.unlock();
     }
  • 读-读能共存、读-写不能共存、写-写不能共存【读锁保护数据的 read() 方法,写锁保护数据的 write() 方法】不同线程间读写是互斥的

  • 读锁不支持条件变量

  • 升级:获取读锁的情况下还想获取写锁; 降级:先获取写锁在获取读锁

  • 重入时升级不支持:持有读锁的情况下去获取写锁会导致获取写锁永久等待,需要先释放读,再去获得写【必须先释放掉读锁】

    • 【可能有多个线程在读,如果其中一个线程想升级,那其他的读线程就 会 很 难 办】

    • 【可以这样理解:因为读锁之间时兼容的,当前线程获得读锁的同时,其他线程可能也获得了读锁】

  • 重入时降级支持 :持有写锁的情况下去获取读锁,造成只有当前线程会持有读锁,因为写锁会互斥其他的锁【写的时候能读,因为写锁只能被一个线程获取】

    • 在保证数据一致性的同时,尽量减少持有写锁的时间

    • 可以做到,因为此线程拿到写锁,只有本线程可以去申请读锁,其他线程拿不到锁了,因为读写不共存。源码中有判断是否时当前线程的if判断。

      • 见w.lock : c != 0 and w == 0 表示有读锁(之前有的),【读锁不能升级】,直接返回 false====从这里也能看出不能升级,也就是原先有了读锁还要尝试tryAcquire获取写锁。(不允许)
    复制代码
     w.lock();
     try {
         r.lock();// 降级为读锁, 释放写锁, 这样能够让其它线程读取缓存
         try {
             // ...
         } finally{
             w.unlock();// 要在写锁释放之前获取读锁
         }
     } finally{
         r.unlock();
     }

在同一个线程内部,情况则有所不同。对于ReentrantReadWriteLock这样的可重入读写锁来说,一个线程是可以先获取独占锁(写锁),然后在不释放该锁的情况下再次获取共享锁(读锁)的。这种情况下,线程仍然持有独占锁,但同时也可以进行读操作(因为它也持有了共享锁)。但请注意,这并不意味着独占锁和共享锁在同一时刻"共存"于同一个线程中,而是线程在内部以特定的方式管理了这两种锁的状态。

构造方法:

  • public ReentrantReadWriteLock():默认构造方法,非公平锁

  • public ReentrantReadWriteLock(boolean fair):true 为公平锁

常用API:

  • public ReentrantReadWriteLock.ReadLock readLock():返回读锁

  • public ReentrantReadWriteLock.WriteLock writeLock():返回写锁

  • public void lock():加锁

  • public void unlock():解锁

  • public boolean tryLock():尝试获取锁

读读并发:

复制代码
 public static void main(String[] args) {
     ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
     ReentrantReadWriteLock.ReadLock r = rw.readLock();
     ReentrantReadWriteLock.WriteLock w = rw.writeLock();
 ​
     new Thread(() -> {
         r.lock();
         try {
             Thread.sleep(2000);
             System.out.println("Thread 1 running " + new Date());
         } finally {
             r.unlock();
         }
     },"t1").start();
     new Thread(() -> {
         r.lock();
         try {
             Thread.sleep(2000);
             System.out.println("Thread 2 running " + new Date());
         } finally {
             r.unlock();
         }
     },"t2").start();
 }

缓存应用

用读写锁实现既能保证一致性,又能不像加普通锁那样性能降低

缓存更新时,是先清缓存还是先更新数据库

  • 先清缓存:可能造成刚清理缓存还没有更新数据库,线程直接查询了数据库更新过期数据到缓存

  • 先更新据库 :可能造成刚更新数据库,还没清空缓存就有线程从缓存拿到了旧数据【 但多做一次查询时可以把错纠正过来

  • 补充情况:查询线程 A 查询数据时恰好缓存数据由于时间到期失效,或是第一次查询

可以使用读写锁进行操作


成员属性原理

读写锁用的是同一个 Sycn(AQS) 同步器,因此等待队列、state 等也是同一个 ,原理与 ReentrantLock 加锁相比没有特殊之处,不同是写锁状态占了 state 的低 16 位,而读锁使用的是 state 的高 16 位【 state分成了两份,还是0--无锁 】

  • 读写锁:

    复制代码
     private final ReentrantReadWriteLock.ReadLock readerLock;       
     private final ReentrantReadWriteLock.WriteLock writerLock;
  • 构造方法:默认是非公平锁,可以指定参数创建公平锁

    复制代码
     public ReentrantReadWriteLock(boolean fair) {
         // true 为公平锁
         sync = fair ? new FairSync() : new NonfairSync();
         // 这两个 lock 共享同一个 sync 实例,都是由 ReentrantReadWriteLock 的 sync 提供同步实现
         readerLock = new ReadLock(this);
         writerLock = new WriteLock(this);
     }

Sync 类的属性:

  • 统计变量:

    复制代码
     // 用来移位
     static final int SHARED_SHIFT   = 16;
     // 高16位的1
     static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
     // 65535,16个1,代表写锁的最大重入次数
     static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
     // 低16位掩码:0b 1111 1111 1111 1111,用来获取写锁重入的次数
     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; }
  • 内部类:

    复制代码
     // 记录读锁线程自己的持有读锁的数量(重入次数),因为 state 高16位记录的是全局范围内所有的读线程获取读锁的总量
     static final class HoldCounter {
         int count = 0;
         // Use id, not reference, to avoid garbage retention
         final long tid = getThreadId(Thread.currentThread());
     }
     // 线程安全的存放线程各自的 HoldCounter 对象
     static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> {
         public HoldCounter initialValue() {
             return new HoldCounter();
         }
     }
  • 内部类实例:

    复制代码
     // 当前线程持有的可重入读锁的数量,计数为 0 时删除
     private transient ThreadLocalHoldCounter readHolds;
     // 记录最后一个获取【读锁】线程的 HoldCounter 对象
     private transient HoldCounter cachedHoldCounter;
  • 首次获取锁:

    复制代码
     // 第一个获取读锁的线程
     private transient Thread firstReader = null;
     // 记录该线程持有的读锁次数(读锁重入次数)
     private transient int firstReaderHoldCount;
  • Sync 构造方法:

    复制代码
     Sync() {
         readHolds = new ThreadLocalHoldCounter();
         // 确保其他线程的数据可见性,state 是 volatile 修饰的变量,重写该值会将线程本地缓存数据【同步至主存】
         setState(getState()); 
     }
相关推荐
尘浮生1 分钟前
Java项目实战II基于Spring Boot的光影视频平台(开发文档+数据库+源码)
java·开发语言·数据库·spring boot·后端·maven·intellij-idea
lldhsds8 分钟前
书生大模型实战营第四期-入门岛-1. Linux前置基础
linux
尚学教辅学习资料9 分钟前
基于SpringBoot的医药管理系统+LW示例参考
java·spring boot·后端·java毕业设计·医药管理
雷神乐乐25 分钟前
File.separator与File.separatorChar的区别
java·路径分隔符
小刘|29 分钟前
《Java 实现希尔排序:原理剖析与代码详解》
java·算法·排序算法
wowocpp31 分钟前
ubuntu 22.04 硬件配置 查看 显卡
linux·运维·ubuntu
JavaNice哥36 分钟前
1初识别jvm
jvm
涛粒子36 分钟前
JVM垃圾回收详解
jvm
YUJIANYUE40 分钟前
PHP将指定文件夹下多csv文件[即多表]导入到sqlite单文件
jvm·sqlite·php