读写锁StampedLock

Java 读写锁

ReentrantReadWriteLock 读写锁

核心特性

  1. 读写分离:分为读锁(共享锁)写锁(独占锁)
  2. 互斥规则
    • 读 + 读:不互斥,多个线程可以同时加读锁
    • 读 + 写:互斥,读锁持有时写锁阻塞,写锁持有时读锁阻塞
    • 写 + 写:互斥,完全独占
  3. 可重入:同一个线程可以重复获取锁
  4. 支持锁降级:写锁可以降级为读锁(先获取写锁 → 获取读锁 → 释放写锁)

致命缺点

读多写少时,会发生 “写线程饥饿”

  • 只要有任意一个线程持有读锁,写线程就会一直阻塞
  • 如果读线程源源不断,写线程永远无法获取锁,导致并发性能瓶颈

StampedLock

JDK 1.8 引入,专为读多写少高并发场景设计,是 ReentrantReadWriteLock 的性能升级版。

1. 三大核心模式

  1. 悲观读锁:和普通读锁一致,共享、与写锁互斥
  2. 写锁:独占锁,和读写锁的写锁一致
  3. 乐观读(核心特性)无锁机制,不加锁,性能极致

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() {
// 1. 【乐观读】无锁,直接获取戳
long stamp = lock.tryOptimisticRead();
double currentX = x;
double currentY = y;

// 2. 校验:期间是否有写操作
if (!lock.validate(stamp)) {
// 3. 数据失效,升级为【悲观读锁】
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

性能对比

  1. 读多写少:StampedLock 性能 远高于 ReentrantReadWriteLock
  2. 写多读少:两者性能几乎一致
  3. 无写操作: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); // 65536
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1; // 高16位/低16位最大值

// 获取读锁数量:state >>> 16
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
// 获取写锁重入数:state & 0x0000FFFF
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); // 写锁重入数

// 1. 有锁(读/写都算)
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;
}

// 2. 无锁:CAS抢写锁
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);
// CAS 增加读锁计数(高16位+1)
if (compareAndSetState(c, c + SHARED_UNIT)) {
// 读锁获取成功
return 1;
}
// CAS失败重试
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;
// 如果是写锁状态,返回0;否则返回版本戳
return (((s = state) & WBITS) == 0L) ? s & SBITS : 0L;
}

// 校验戳:判断期间是否有写操作
public boolean validate(long stamp) {
// 读屏障,保证可见性
VarHandle.acquireFence();
// 戳没变 → 无写操作
return (stamp & SBITS) == (state & SBITS);
}

原理

  1. 乐观读完全无锁
  2. 读取前拿一个戳
  3. 读完校验戳是否变化
  4. 变化 = 有写操作,需要升级悲观读

写锁 / 悲观读锁 源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 写锁:独占,CAS抢锁
public long writeLock() {
long s, next;
do {
s = state;
next = s + WBIT; // 写锁标记
} while (!compareAndSetState(s, next));
return next;
}

// 悲观读锁:共享,计数+1
public long readLock() {
long s, next;
do {
s = state;
next = s + RBIT; // 读锁计数+1
} while (!compareAndSetState(s, next));
return next;
}

StampedLock 关键优势

乐观读完全无锁:读不阻塞写,写不阻塞读

无 AQS 队列:轻量级实现,性能更高

解决写饥饿:读锁不会长期霸占锁,写线程随时可以抢占


读写锁StampedLock
http://hanqichuan.com/2026/04/16/java并发/读写锁StampedLock/
作者
韩启川
发布于
2026年4月16日
许可协议