轩辕李的博客 轩辕李的博客
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • HTML&CSS
  • JavaScript
  • 分布式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

轩辕李

勇猛精进,星辰大海
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • HTML&CSS
  • JavaScript
  • 分布式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Java

    • 核心

    • 并发

      • Java并发-线程基础与synchronized关键字
      • Java并发-重入锁ReentrantLock详解与实践
      • Java并发-信号量Semaphore
      • Java并发-读写锁ReadWriteLock
        • 一、什么是读写锁?
        • 二、实现
        • 三、读写锁的关键特性
        • 四、StampedLock:更高性能的选择
          • 1、的三种锁模式
          • 2、实际应用示例
          • 3、乐观读的工作原理
          • 4、性能优势
        • 五、如何选择合适的锁?
      • Java并发-倒计时器CountDownLatch
      • Java并发-栅栏CyclicBarrier
      • Java并发-LockSupport线程阻塞工具类
      • Java并发-线程池ThreadPoolExecutor
      • Java并发-阻塞队列BlockingQueue
      • Java并发-以空间换时间之ThreadLocal
      • Java并发-无锁策略CAS与atomic包
      • Java并发-JDK并发容器
      • Java并发-异步调用结果之Future和CompletableFuture
      • Java并发-Fork Join框架
      • Java并发-调试与诊断
    • 经验

    • JVM

    • 企业应用

  • Spring

  • 其他语言

  • 工具

  • 后端
  • Java
  • 并发
轩辕李
2018-06-08
目录

Java并发-读写锁ReadWriteLock

在并发编程中,我们经常遇到读多写少的场景。比如缓存系统、配置管理、统计计数器等,这些场景下读操作的频率远远超过写操作。如果简单地用 synchronized 或 ReentrantLock 来处理,虽然能保证线程安全,但性能会受到很大影响。

举个例子,假设我们有一个简单的数据容器:

public static class SimpleContainer {
    private int value;

    public synchronized int read() throws InterruptedException {
        // 模拟读操作耗时
        Thread.sleep(1000);
        return value;
    }

    public synchronized void write(int newValue) throws InterruptedException {
        // 模拟写操作耗时
        Thread.sleep(1000);
        this.value = newValue;
    }
}

这种实现虽然简单,但存在一个明显的问题:即使是多个线程同时读取数据(理论上不会产生数据竞争),它们也必须排队等待,无法并发执行。这就是读写锁要解决的核心问题。

# 一、什么是读写锁?

读写锁(ReadWriteLock)是一种特殊的锁机制,它将对共享资源的访问分为两种类型:

  • 读锁(共享锁):多个线程可以同时持有读锁,互不冲突
  • 写锁(独占锁):只有一个线程可以持有写锁,且与读锁互斥

这种设计的核心思想是:读读不冲突,读写冲突,写写冲突。

# 二、实现

Java 提供了 ReentrantReadWriteLock 作为 ReadWriteLock 接口的标准实现。让我们看看如何使用它:

public static class ReadWriteContainer {
    private int value;
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private final Lock readLock = lock.readLock();
    private final Lock writeLock = lock.writeLock();

    public int read() throws InterruptedException {
        readLock.lock();
        try {
            // 模拟读操作耗时
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + " 正在读取: " + value);
            return value;
        } finally {
            readLock.unlock();
        }
    }

    public void write(int newValue) throws InterruptedException {
        writeLock.lock();
        try {
            // 模拟写操作耗时
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + " 正在写入: " + newValue);
            this.value = newValue;
        } finally {
            writeLock.unlock();
        }
    }
}

使用示例:

public static void main(String[] args) {
    ReadWriteContainer container = new ReadWriteContainer();
    
    // 启动 2 个写线程
    for (int i = 0; i < 2; i++) {
        final int writeValue = i + 1;
        new Thread(() -> {
            try {
                container.write(writeValue);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }, "Writer-" + i).start();
    }
    
    // 启动 5 个读线程
    for (int i = 0; i < 5; i++) {
        new Thread(() -> {
            try {
                container.read();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }, "Reader-" + i).start();
    }
}

运行这段代码,你会发现:

  • 多个读线程可以并发执行,不需要相互等待
  • 写线程会独占访问,其他所有操作都被阻塞
  • 读写之间互斥,有写操作时读操作会等待,反之亦然

这样的设计在读多写少的场景下,性能提升是非常明显的。想象一下,如果有 100 个读请求和 1 个写请求,传统互斥锁需要顺序执行 101 次操作,而读写锁允许 100 个读操作并发执行,只有写操作需要独占。

# 三、读写锁的关键特性

  1. 公平性选择:ReentrantReadWriteLock 支持公平和非公平模式
  2. 重入性:同一线程可以多次获取读锁或写锁
  3. 锁降级:持有写锁的线程可以获取读锁,然后释放写锁(但不支持锁升级)
  4. 条件变量:写锁支持条件变量,读锁不支持

# 四、StampedLock:更高性能的选择

Java 8 引入了 StampedLock,这是对传统读写锁的重要改进。传统读写锁虽然允许多个读操作并发,但读写之间依然是互斥的。StampedLock 通过引入乐观读的概念,进一步提升了性能。

# 1、的三种锁模式

  1. 写锁:独占锁,与传统读写锁的写锁类似
  2. 悲观读锁:共享锁,与传统读写锁的读锁类似
  3. 乐观读:这是 StampedLock 的核心创新,不是真正的锁

# 2、实际应用示例

public class Point {
    private double x, y;
    private final StampedLock lock = new StampedLock();

    // 写操作:使用独占锁
    public void move(double deltaX, double deltaY) {
        long stamp = lock.writeLock();
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            lock.unlockWrite(stamp);
        }
    }

    // 读操作:先尝试乐观读,失败则降级为悲观读
    public double distanceFromOrigin() {
        long stamp = lock.tryOptimisticRead();  // 尝试乐观读
        
        // 在乐观读状态下读取数据
        double curX = x;
        double curY = y;
        
        // 检查期间是否有写操作
        if (!lock.validate(stamp)) {
            // 乐观读失败,升级为悲观读锁
            stamp = lock.readLock();
            try {
                curX = x;
                curY = y;
            } finally {
                lock.unlockRead(stamp);
            }
        }
        
        return Math.sqrt(curX * curX + curY * curY);
    }
    
    // 只读操作的另一种实现方式
    public double getX() {
        long stamp = lock.tryOptimisticRead();
        double currentX = x;
        
        // 如果期间没有写操作,直接返回
        if (lock.validate(stamp)) {
            return currentX;
        }
        
        // 否则使用悲观读锁
        stamp = lock.readLock();
        try {
            return x;
        } finally {
            lock.unlockRead(stamp);
        }
    }
}

# 3、乐观读的工作原理

  1. 获取戳记:通过 tryOptimisticRead() 获取一个 stamp(戳记)
  2. 读取数据:在没有锁保护的情况下读取共享变量
  3. 验证戳记:通过 validate(stamp) 检查期间是否有写操作
  4. 处理结果:
    • 如果验证成功,说明读取的数据是有效的
    • 如果验证失败,需要重新读取或升级为悲观锁

# 4、性能优势

StampedLock 的乐观读机制带来了显著的性能提升:

  • 无锁开销:乐观读不需要获取实际的锁,避免了锁竞争
  • 更好的并发性:读操作不会阻塞写操作的获取
  • 适应性强:在读多写少的场景下,大部分读操作都能通过乐观读完成

但要注意,StampedLock 也有一些限制:

  • 不支持重入
  • 不支持条件变量
  • 使用复杂度相对较高

# 五、如何选择合适的锁?

根据不同的应用场景,我们可以这样选择:

场景 推荐方案 理由
读写比例相当 synchronized 或 ReentrantLock 实现简单,性能够用
读多写少,对性能要求不高 ReentrantReadWriteLock 平衡了复杂度和性能
读多写少,对性能要求很高 StampedLock 最佳性能,但实现复杂
需要重入或条件变量 ReentrantReadWriteLock StampedLock 不支持这些特性

总的来说,读写锁是并发编程中的重要工具,正确选择和使用它们,能够在保证线程安全的前提下显著提升应用性能。

编辑 (opens new window)
#ReadWriteLock
上次更新: 2025/08/14
Java并发-信号量Semaphore
Java并发-倒计时器CountDownLatch

← Java并发-信号量Semaphore Java并发-倒计时器CountDownLatch→

最近更新
01
AI时代的编程心得
09-11
02
Claude Code与Codex的协同工作
09-01
03
Claude Code实战之供应商切换工具
08-18
更多文章>
Theme by Vdoing | Copyright © 2018-2025 京ICP备2021021832号-2 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式