关于ReadWriteLock读写锁的介绍

#新星杯·14天创作挑战营·第11期#

目录

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;

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


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、核心思想

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

  1. 读锁(共享锁)

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

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

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

代码示例:

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位)表示写锁重入次数。
  • readLock 和 writeLock:分别管理读锁和写锁的获取与释放。

以下是常用的方法:

  • 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 队列
  • 非公平模式下
    • 读线程可以“插队”获取锁(无需排队)。
    • 写线程只能按顺序等待,直到没有读线程。

示例:

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)
  • 效果
    • 写线程按顺序获取锁,不会被读线程插队。
    • 优点:避免写锁饥饿。
    • 缺点:性能略低(读线程无法插队)。

代码示例:

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

总结:

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值