关于ReadWriteLock读写锁的介绍

目录

1、普通锁

1.1、原理

1.2、特点

2、ReadWriteLock

2.1、核心思想

2.2、特点

1、高效

2、缓存读取和更新

2.3、锁共存

[1. 数据一致性要求](#1. 数据一致性要求)

[2. 内部实现限制](#2. 内部实现限制)

2.4、关键字段

2.5、获取流程

1、写锁

2、读锁

3、写锁饥饿

3.1、原因

[1. 优先级](#1. 优先级)

[2. 等待队列机制](#2. 等待队列机制)

3.2、实现原理

[1. 写锁获取流程](#1. 写锁获取流程)

[2. 写锁释放流程](#2. 写锁释放流程)

3.3、避免写锁饥饿

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

[2. 限制读锁的持有时间](#2. 限制读锁的持有时间)

[3. 使用 StampedLock](#3. 使用 StampedLock)


前言

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

关于更多锁的介绍,可参考:Java常用锁的实践_java常用的锁-CSDN博客


1、普通锁

读写互斥,如 ReentrantLock。

1.1、原理

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

代码示例:

java 复制代码
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、核心思想

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

  1. 读锁(共享锁)

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

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

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

代码示例:

java 复制代码
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、缓存读取和更新

java 复制代码
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、写锁

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

2、读锁

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

小结

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

  • 选择普通锁

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

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

对比

普通锁 vs ReadWriteLock:


3、写锁饥饿

3.1、原因

1. 优先级

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

2. 等待队列机制

  • AQS(AbstractQueuedSynchronizer)维护一个 FIFO 队列
  • 非公平模式下
    • 读线程可以"插队"获取锁(无需排队)。
    • 写线程只能按顺序等待,直到没有读线程。

示例:

java 复制代码
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

// 线程 A: 读线程
lock.readLock().lock();
try {
    while (true) {
        // 持续读取(不释放读锁)
    }
} finally {
    lock.readLock().unlock();
}

// 线程 B: 写线程
lock.writeLock().lock(); // 被阻塞,永远无法获取写锁

3.2、实现原理

1. 写锁获取流程

  1. 检查当前是否有写锁 (通过 exclusiveCount 判断)。
  2. 检查是否有读锁 (通过 sharedCount 判断)。
  3. 非公平模式下
    • 如果没有写锁,且当前线程可以插队(无需等待),则直接获取写锁。
    • 如果有读锁或写锁,则将线程加入等待队列。
  4. 公平模式下
    • 写线程必须按顺序等待,即使没有读锁。

2. 写锁释放流程

  1. 释放写锁后,唤醒等待队列中的线程。
  2. 非公平模式下
    • 新来的读线程可能再次插队获取读锁。
    • 写线程仍需等待所有读线程释放读锁。

3.3、避免写锁饥饿

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

  • 配置公平锁new ReentrantReadWriteLock(true)
  • 效果
    • 写线程按顺序获取锁,不会被读线程插队。
    • 优点:避免写锁饥饿。
    • 缺点:性能略低(读线程无法插队)。

代码示例:

java 复制代码
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 提供更灵活的读写锁策略

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

代码示例:

java 复制代码
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);
    }
}

总结:

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

相关推荐
伍六星11 分钟前
基于JDBC的信息管理系统,那么什么是JDBC呢?
java·数据库·后端·jdbc·数据库连接
互联网行者1 小时前
java云原生实战之graalvm 环境安装
java·开发语言·云原生
LinuxSuRen4 小时前
Docker Compose Provider 服务介绍
java·运维·docker·容器·eureka
sg_knight4 小时前
Docker网络全景解析:Overlay与Macvlan深度实践,直通Service Mesh集成核心
java·网络·spring boot·spring cloud·docker·容器·service_mesh
学习使我变快乐5 小时前
C++:迭代器
开发语言·c++·windows
鬣主任5 小时前
JavaSenderMail发送邮件(QQ及OFFICE365)
java·spring boot·smtp/imap/tls
zwjapple6 小时前
RabbitMQ的基本使用
开发语言·后端·ruby
咖啡の猫6 小时前
JavaScript基础-作用域链
开发语言·javascript
佩奇的技术笔记6 小时前
Python入门手册:Python简介,什么是Python
开发语言·python