Java 读写锁
ReentrantReadWriteLock 读写锁
核心特性
- 读写分离:分为读锁(共享锁)和写锁(独占锁)
- 互斥规则
- 读 + 读:不互斥,多个线程可以同时加读锁
- 读 + 写:互斥,读锁持有时写锁阻塞,写锁持有时读锁阻塞
- 写 + 写:互斥,完全独占
- 可重入:同一个线程可以重复获取锁
- 支持锁降级:写锁可以降级为读锁(先获取写锁 → 获取读锁 → 释放写锁)
致命缺点
读多写少时,会发生 “写线程饥饿”
- 只要有任意一个线程持有读锁,写线程就会一直阻塞
- 如果读线程源源不断,写线程永远无法获取锁,导致并发性能瓶颈
StampedLock
JDK 1.8 引入,专为读多写少高并发场景设计,是 ReentrantReadWriteLock 的性能升级版。
1. 三大核心模式
- 悲观读锁:和普通读锁一致,共享、与写锁互斥
- 写锁:独占锁,和读写锁的写锁一致
- 乐观读(核心特性):无锁机制,不加锁,性能极致
2. 核心原理:乐观读
- 不加锁:读取共享变量时,不获取任何锁
- 版本戳(stamp):读取时获取一个时间戳,读取完成后校验戳是否变化
- 校验逻辑:
- 戳没变 → 期间无写操作,数据有效
- 戳变了 → 期间有写操作,数据无效,需要升级为悲观读锁重试
3. 关键特点
- 不可重入:不支持锁重入(使用时必须注意,避免死锁)
- 不支持条件变量(Condition)
- 读完全不阻塞写:彻底解决写线程饥饿问题
示例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| import java.util.concurrent.locks.StampedLock;
public class StampedLockDemo { private double x, y; private final StampedLock lock = new StampedLock();
public void update(double deltaX, double deltaY) { long stamp = lock.writeLock(); try { x += deltaX; y += deltaY; } finally { lock.unlockWrite(stamp); } }
public double calculate() { long stamp = lock.tryOptimisticRead(); double currentX = x; double currentY = y;
if (!lock.validate(stamp)) { stamp = lock.readLock(); try { currentX = x; currentY = y; } finally { lock.unlockRead(stamp); } } return Math.sqrt(currentX * currentX + currentY * currentY); } }
|
适用场景与性能对比
适用场景
| 锁类型 |
最佳适用场景 |
优势 |
劣势 |
| ReentrantReadWriteLock |
读写均衡、读少写多、需要重入 / Condition |
可重入、支持锁降级、API 成熟 |
读多写少会造成写线程饥饿 |
| StampedLock |
读极多、写极少(高并发读) |
乐观读无锁、性能极致、写不饥饿 |
不可重入、API 复杂、不支持 Condition |
性能对比
- 读多写少:StampedLock 性能 远高于 ReentrantReadWriteLock
- 写多读少:两者性能几乎一致
- 无写操作:StampedLock 乐观读 = 无锁操作,性能天花板
实现原理
ReentrantReadWriteLock 源码原理
一个 state 变量切分 读写状态
state是 32 位 int,高 16 位存读锁数量,低 16 位存写锁重入次数
1 2 3 4 5 6 7 8 9
| static final int SHARED_SHIFT = 16; static final int SHARED_UNIT = (1 << SHARED_SHIFT); static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
static int exclusiveCount(int c) { return c & MAX_COUNT; }
|
写锁(独占锁)源码 tryAcquire
写锁是独占锁,只有 0/1 两种持有状态,支持重入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| protected final boolean tryAcquire(int acquires) { Thread current = Thread.currentThread(); int c = getState(); int w = exclusiveCount(c);
if (c != 0) { if (w == 0 || current != getExclusiveOwnerThread()) return false; if (w + exclusiveCount(acquires) > MAX_COUNT) throw new Error("Maximum lock count exceeded"); setState(c + acquires); return true; } if (compareAndSetState(c, c + acquires)) { setExclusiveOwnerThread(current); return true; } return false; }
|
写锁规则:
- 有读锁 → 写锁绝对抢不到
- 有写锁 → 只有持有线程可重入
- 无锁 → CAS 竞争写锁
读锁(共享锁)源码 tryAcquireShared
读锁共享,多个线程可同时获取:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| protected final int tryAcquireShared(int unused) { Thread current = Thread.currentThread(); int c = getState();
if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) return -1;
int r = sharedCount(c); if (compareAndSetState(c, c + SHARED_UNIT)) { return 1; } return -1; }
|
致命缺陷:
只要有写锁被持有,所有读线程全部阻塞;
只要有读锁被持有,写线程永远阻塞 → 写线程饥饿
StampedLock 源码原理
StampedLock 没有继承 AQS,是 JDK1.8 全新实现,核心是 版本戳 + 无锁乐观读。
核心数据结构
1
| private transient volatile long state;
|
不是 int,是 long
用不同 bit 位表示:写锁、悲观读锁、乐观读版本
核心概念:Stamp(戳)
每次加锁 / 释放锁都会返回 / 校验一个long类型戳:
writeLock() → 返回写戳
readLock() → 返回读戳
tryOptimisticRead() → 返回乐观读戳
乐观读 源码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public long tryOptimisticRead() { long s; return (((s = state) & WBITS) == 0L) ? s & SBITS : 0L; }
public boolean validate(long stamp) { VarHandle.acquireFence(); return (stamp & SBITS) == (state & SBITS); }
|
原理:
- 乐观读完全无锁
- 读取前拿一个戳
- 读完校验戳是否变化
- 变化 = 有写操作,需要升级悲观读
写锁 / 悲观读锁 源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public long writeLock() { long s, next; do { s = state; next = s + WBIT; } while (!compareAndSetState(s, next)); return next; }
public long readLock() { long s, next; do { s = state; next = s + RBIT; } while (!compareAndSetState(s, next)); return next; }
|
StampedLock 关键优势
乐观读完全无锁:读不阻塞写,写不阻塞读
无 AQS 队列:轻量级实现,性能更高
解决写饥饿:读锁不会长期霸占锁,写线程随时可以抢占