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

 更新时间:2025年06月30日 09:22:04   作者:找不到、了  
这篇文章主要介绍了关于ReadWriteLock读写锁的使用及说明,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教

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

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低位增加)。
  • 写锁可降级为读锁(但不能升级为写锁)。

锁升级/降级规则

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

代码示例:

ReadWriteLock 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();
    }
}

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

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

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:分别管理读锁和写锁的获取与释放。

以下是常用的方法:

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

2.5、获取流程

1、写锁

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

2、读锁

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

小结

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

选择普通锁

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

选择读写锁

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

对比

普通锁 vsReadWriteLock:

3、写锁饥饿

3.1、原因

1. 优先级

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

2. 等待队列机制

AQS(AbstractQueuedSynchronizer)维护一个 FIFO 队列

非公平模式下

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

示例:

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();
    }
}

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

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);
    }
}

总结

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

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • Java 事务注解@Transactional回滚(try catch、嵌套)问题

    Java 事务注解@Transactional回滚(try catch、嵌套)问题

    这篇文章主要介绍了Java @Transactional回滚(try catch、嵌套)问题,Spring 事务注解 @Transactional 本来可以保证原子性,如果事务内有报错的话,整个事务可以保证回滚,但是加上try catch或者事务嵌套,可能会导致事务回滚失败
    2022-08-08
  • 一文带你了解SpringBoot的启动原理

    一文带你了解SpringBoot的启动原理

    大家通常只需要给一个类添加一个@SpringBootApplication 注解,然后再加一个main 方法里面固定的写法 SpringApplication.run(Application.class, args);那么spring boot 到底是如何启动服务的呢,接下来咱们通过源码解析,需要的朋友可以参考下
    2023-05-05
  • Java实现自定义自旋锁代码实例

    Java实现自定义自旋锁代码实例

    这篇文章主要介绍了Java实现自定义自旋锁代码实例,Java自旋锁是一种线程同步机制,它允许线程在获取锁时不立即阻塞,而是通过循环不断尝试获取锁,直到成功获取为止,自旋锁适用于锁竞争激烈但持有锁的时间很短的情况,需要的朋友可以参考下
    2023-10-10
  • java 排序算法之快速排序

    java 排序算法之快速排序

    这篇文章主要介绍了java 排序算法之快速排序,文中通过图片和代码讲解相关知识非常详细,大家如果有需要的话可以参考一下这篇文章
    2021-09-09
  • SpringBoot项目打成War包部署的方法步骤

    SpringBoot项目打成War包部署的方法步骤

    这篇文章主要介绍了springboot项目如何打war包流程的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-12-12
  • Thymeleaf中th:each及th:if使用方法解析

    Thymeleaf中th:each及th:if使用方法解析

    这篇文章主要介绍了Thymeleaf中th:each及th:if使用方法解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-08-08
  • Java中的== 和equals()方法详解与实例

    Java中的== 和equals()方法详解与实例

    本篇文章介绍了,在java中"==" 与equals方法的使用及其实例,需要的朋友可以参考下
    2017-04-04
  • mybatis plus 关联数据库排除不必要字段方式

    mybatis plus 关联数据库排除不必要字段方式

    这篇文章主要介绍了mybatis plus 关联数据库排除不必要字段方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-03-03
  • String类的获取功能、转换功能

    String类的获取功能、转换功能

    这篇文章给大家介绍了String类的获取功能:String类的基本获取功能、获取功能的举例子、String类的基本转换功能、转换功能的举例子。具体详情大家参考下本文
    2018-04-04
  • SpringBoot对静态资源的映射规则详解

    SpringBoot对静态资源的映射规则详解

    在Web应用中会涉及到大量的静态资源,例如 JS、CSS和HTML等,我们知道,Spring MVC 导入静态资源文件时,需要配置静态资源的映射,但在 SpringBoot 中则不再需要进行此项配置,因为SpringBoot已经默认完成了这一工作,本文给大家介绍了SpringBoot对静态资源的映射规则详
    2024-12-12

最新评论