开发者

关于ReadWriteLock读写锁的使用及说明

开发者 https://www.devze.com 2025-06-30 10:41 出处:网络 作者: 找不到、了
目录1、普通锁1.1、原理1.2、特点2、ReadwriteLock2.1、核心思想2.2、特点1、高效2、缓存读取和更新2.3、锁共存1. 数据一致性要求2. 内部实现限制2.4、关键字段2.5、获取流程1、写锁2、读锁3、写锁饥饿3.1、原因1. 优
目录
  • 1、普通锁
    • 1.1、原理
    • 1.2、特点
  • 2、ReadwriteLock
    • 2.1、核心思想
    • 2.2、特点
      • 1、高效
      • 2、缓存读取和更新
    • 2.3、锁共存
      • 1. 数据一致性要求
      • 2. 内部实现限制
    • 2.4、关键字段
      • 2.5、获取流程
        • 1、写锁
        • 2、读锁
    • 3、写锁饥饿
      • 3.1、原因
        • 1. 优先级
        • 2. 等待队列机制
      • 3.2、实现原理
        • 1. 写锁获取流程
        • 2. 写锁释放流程
      • 3.3、避免写锁饥饿
        • 1. 使用公平模式(Fair Mode)
        • 2.限制读锁的持有时间
        • 3. 使用StampedLock
    • 总结

      ReentrantReadWriteLock实现了ReadWriteLock接口。位于Java.util.concurrent.locks;

      关于ReadWriteLock读写锁的使用及说明

      1、普通锁

      读写互斥,如ReentrantLock。

      1.1、原理

      • 普通锁是排他锁(Exclusive Lock):无论读还是写,同一时刻只能有一个线程持有锁。
      • 所有操作互斥:即使多个线程只是读取数据,普通锁也会阻塞其他线程。

      代码示例:

      ReentrantLock lock = new ReentrantLock();
      
      void read() {
          lock.lock();
          try {
              // 读取数据
          } finally {
              lock.unlock();
          }
      }
      
      void write() {
          lock.lock();
          try {
              // 写入数据
          } finally {
              lock.unlock();
          }
      }
      

      1.2、特点

      • 读线程会阻塞其他读线程:即使没有写操作,读线程之间也不能并发。
      • 性能低:在高并发读场景下,资源利用率低。

      2、ReadWriteLock

      读写分离机制。

      • 基于 AQS:通过state字段的高位和低位分别管理读锁和写锁。
      • 共享锁(Shared):允许多个线程同时读。
      • 排他锁(Exclusive):写操作独占锁。

      2.1、核心思想

      规则读锁与读锁不互斥读锁与写锁互斥写锁与写锁互斥

      读锁(共享锁)

      • 多个线程可同时持有读锁。
      • 获取读锁时,需确保没有写锁存在。
      • 读锁可重入(同一线程多次获取读锁时,state高位增加)。

      写锁(排他锁)

      • 写锁独占,阻塞所有读和写操作。
      • 写锁可重入(同一线程多次获取写锁时,state低位增加)。
      • 写锁可降级为读锁(但不能升级为写锁)。

      锁升级/降级规则

      • 不允许升级:读锁不能直接升级为写锁(会破坏公平性,可能导致死锁)。
      • 允许降级:写锁可以降级为读锁(需显式释放写锁后获取读锁)。

      代码示例:

      ReadWriteLwww.devze.comock readWriteLock = new ReentrantReadWriteLock();
      Lock readLock = readWriteLock.readLock();
      Lock writeLock = readWriteLock.writeLock();
      
      void read() {
          readLock.lock();
          try {
              // 读取数据(多个线程可同时读)
          } finally {
              readLock.unlock();
          }
      }
      
      void write() {
          writeLock.lock();
          try {
              // 写入数据(独占)
          } finally {
              writeLock.unlock();
          }
      }
      

      为什么读锁和写锁可以“部分共存”?

      • 读锁不阻塞其他读锁:因为读操作不会修改数据,多个线程读取共享数据是安全的。
      • 写锁阻塞所有读写:写操作需要独占数据,防GTJnNB止脏读和数据不一致。

      2.2、特点

      1、高效

      适合高并发读的场景。

      • 普通锁:多个读线程互相阻塞,吞吐量低。
      • 读写锁:多个读线程可并发读取,吞吐量高。

      2、缓存读取和更新

      class Cache {
          private Object data;
          private ReadWriteLock lock = new ReentrantReadWriteLock();
      
          void get() {
              lock.readLock().lock();
              try {
                  // 多个线程可同时读取
                  return data;
              } finally {
                  lock.readLock().unlock();
              }
          }
      
          void put(Object newData) {
              lock.writeLock().lock();
              try {
                  // 写入时独占
                  data = newData;
              } finally {
                  lock.writeLock().unlock();
              }
          }
      }
      
      • 优势:缓存读取频繁,写入较少,使用读写锁可大幅提升并发性能。

      2.3、锁共存

      写锁不能与读锁或写锁共存。具体是为什么,可参考以下数据一致性和state字段来进行分析。

      1. 数据一致性要求

      写操作必须独占:如果允许写锁与读锁或写锁共存,可能导致:

      • 脏读:读线程读到未提交的数据。
      • 数据不一致:多个写线程同时修改数据,导致结果不可预测。

      2. 内部实现限制

      读写锁的实现

      • 使用一个int类型的state字段,高16位表示读锁数量,低16位表示写锁重入次数。
      • 写锁获取时:必须确保当前没有读锁或写锁。
      • 读锁获取时:必须确保当前没有写锁。

      2.4、关键字段

      • state:高位(32位)表示读锁数量,低位(32位)表示写锁重入次数。
      • readLockwriteLock:分别管理读锁和www.devze.com写锁的获取与释放。

      以下是常用的方法:

      • readLock().lock():尝试获取共享锁。
      • writeLock().lock():尝试获取排他锁。
      • readLock().unlock()writeLock().unlock():释放对应锁。

      2.5、获取流程

      1、写锁

      • 检查当前是否有写锁(通过exclusiveCount判断)。
      • 检查是否有读锁(通过sharedCount判断)。
      • 如果没有读锁和写锁,则设置写锁状态。
      • 否则,将线程加入等待队列。

      2、读锁

      • 检查当前是否有写锁。
      • 如果没有写锁,则尝试增加读锁计数。
      • 如果有写锁或读锁溢出,则将线程加入等待队列。

      小结

      如何选择哪种锁,可根据以下场景进行分析:

      编程客栈择普通锁

      • 数据操作简单(如单次写入后只读)。
      • 不需要区分读写操作。

      选择读写锁

      • 读操作远多于写操作(如缓存、配置中心)。
      • 需要提升读并发性能。

      对比

      普通锁 vsReadWriteLock:

      关于ReadWriteLock读写锁的使用及说明

      3、写锁饥饿

      3.1、原因

      1. 优先级

      • ReentrantReadWriteLock 默认是非公平模式fair=false)。
      • 读锁的优先级更高:在非公平模式下,读锁可以“插队”获取锁,即使有等待的写线程。
      • 写锁需要独占锁:写操作必须阻塞所有读和写,因此写线程会一直等待,直到所有读线程释放读锁。

      2. 等待队列机制

      AQS(AbstractQueuedSynchronizer)维护一个 FIFO 队列

      非公平模式下GTJnNB

      • 读线程可以“插队”获取锁(无需排队)。
      • 写线程只能按顺序等待,直到没有读线程。

      示例:

      ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
      
      // 线程 A: 读线程
      lock.readLock().lock();
      try {
          while (true) {
              // 持续读取(不释放读锁)
          }
      } finally {
          lock.readLock().unlock();
      }
      
      // 线程 B: 写线程
      lock.writeLock().lock(); // 被阻塞,永远无法获取写锁
      

      3.2、实现原理

      1. 写锁获取流程

      检查当前是否有写锁(通过exclusiveCount判断)。

      检查是否有读锁(通过sharedCount判断)。

      非公平模式下

      • 如果没有写锁,且当前线程可以插队(无需等待),则直接获取写锁。
      • 如果有读锁或写锁,则将线程加入等待队列。

      公平模式下

      • 写线程必须按顺序等待,即使没有读锁。

      2. 写锁释放流程

      1. 释放写锁后,唤醒等待队列中的线程。

      非公平模式下

      • 新来的读线程可能再次插队获取读锁。
      • 写线程仍需等待所有读线程释放读锁。

      3.3、避免写锁饥饿

      1. 使用公平模式(Fair Mode)

      • 配置公平锁new ReentrantReadWriteLock(true)

      效果

      • 写线程按顺序获取锁,不会被读线程插队。
      • 优点:避免写锁饥饿。
      • 缺点:性能略低(读线程无法插队)。

      代码示例:

      ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true); // 公平模式
      
      void read() {
          lock.readLock().lock();
          try {
              // 读取数据
          } finally {
              lock.readLock().unlock();
          }
      }
      
      void write() {
          lock.writeLock().lock();
          try {
              // 写入数据
          } finally {
              lock.writeLock().unlock();
          }
      }
      

      公平模式下和非公平模式下:

      关于ReadWriteLock读写锁的使用及说明

      2.限制读锁的持有时间

      避免读线程长期占用读锁

      • 在业务逻辑中控制读锁的持有时间。
      • 避免在读锁内执行长时间操作。

      3. 使用StampedLock

      在Java 8+,StampedLock提供更灵活的读写锁策略

      • 支持乐观读锁(不阻塞写锁)。
      • 支持写锁优先级(避免读锁插队)。

      代码示例:

      StampedLock lock = new StampedLock();
      
      void read() {
          long stamp = lock.tryOptimisticRead();
          if (lock.validate(stamp)) {
              // 乐观读取(不阻塞写锁)
          }
      }
      
      void write() {
          long stamp = lock.writeLock();
          try {
              // 写入数据
          } finally {
              lock.unlockWrite(stamp);
          }
      }
      

      总结

      关于ReadWriteLock读写锁的使用及说明

      通过合理选择锁策略,可以在高并发场景下平衡性能与公平性!

      以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程客栈(www.devze.com)。

      0

      精彩评论

      暂无评论...
      验证码 换一张
      取 消

      关注公众号