目录
- 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;
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位)表示写锁重入次数。readLock
和writeLock
:分别管理读锁和www.devze.com写锁的获取与释放。
以下是常用的方法:
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 队列。
非公平模式下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. 写锁释放流程
- 释放写锁后,唤醒等待队列中的线程。
非公平模式下:
- 新来的读线程可能再次插队获取读锁。
- 写线程仍需等待所有读线程释放读锁。
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); } }
总结
通过合理选择锁策略,可以在高并发场景下平衡性能与公平性!
以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程客栈(www.devze.com)。
精彩评论