公平的代价是性能,性能的代价是公平。如何在两者之间找到平衡?让我们从零开始,打造一个读写公平的锁!
一、开场:什么是公平锁?🤔
公平 vs 非公平
非公平锁(Unfair Lock):
- 线程抢锁,谁抢到算谁的
- 可能导致饥饿:某些线程永远抢不到
公平锁(Fair Lock):
- 线程排队,先来先服务(FIFO)
- 保证每个线程都能获得锁
生活类比:
非公平锁像抢票🎫:
- 售票窗口一开,大家一拥而上
- 年轻人跑得快,总是抢到
- 老年人可能永远买不到
公平锁像排队🚶:
- 按顺序排队买票
- 先到先得,公平但慢
二、读写锁的公平性问题📚
ReentrantReadWriteLock的困境
问题1:写线程饥饿
ReadWriteLock lock = new ReentrantReadWriteLock(false); // 非公平
// 100个读线程不停读
for (int i = 0; i < 100; i++) {
new Thread(() -> {
while (true) {
lock.readLock().lock();
try {
// 读操作
} finally {
lock.readLock().unlock();
}
}
}).start();
}
// 1个写线程
new Thread(() -> {
lock.writeLock().lock(); // 永远获取不到!😭
try {
// 写操作
} finally {
lock.writeLock().unlock();
}
}).start();
问题2:读线程饥饿
ReadWriteLock lock = new ReentrantReadWriteLock(true); // 公平模式
// 1个写线程频繁写
new Thread(() -> {
while (true) {
lock.writeLock().lock();
try {
Thread.sleep(10);
} finally {
lock.writeLock().unlock();
}
}
}).start();
// 100个读线程
// 读线程必须等待前面的写线程,即使是读操作!
公平模式的性能问题:
- 读线程之间本可以并发
- 但为了公平,必须排队
- 性能下降严重
三、设计目标🎯
我们要实现什么?
- 读读不互斥:多个读线程可以同时持有锁
- 读写互斥:读和写不能同时
- 写写互斥:写和写不能同时
- 公平性:
- 读线程不能"插队"到写线程前面
- 写线程也不能一直霸占锁
- 防止饥饿:所有线程都能获得锁
核心思想
策略:写者优先 + 队列
队列: [读1] [写1] [读2] [读3] [写2]
当前: 读1持有锁
→ 写1在等待(后面的读2、读3不能插队!)
→ 读1释放后,写1获取锁
→ 写1释放后,读2和读3同时获取锁
→ 读2、读3释放后,写2获取锁
四、版本1:基础读写锁(无公平性)📝
实现
public class SimpleReadWriteLock {
private int readers = 0; // 当前读线程数
private int writers = 0; // 当前写线程数(0或1)
private int writeRequests = 0; // 等待的写线程数
// 读锁
public synchronized void lockRead() throws InterruptedException {
while (writers > 0 || writeRequests > 0) {
wait(); // 有写线程,等待
}
readers++;
}
public synchronized void unlockRead() {
readers--;
notifyAll(); // 唤醒等待的线程
}
// 写锁
public synchronized void lockWrite() throws InterruptedException {
writeRequests++;
while (readers > 0 || writers > 0) {
wait(); // 有读或写线程,等待
}
writeRequests--;
writers++;
}
public synchronized void unlockWrite() {
writers--;
notifyAll();
}
}
问题:
- ❌ 无法区分等待的顺序
- ❌ 可能导致饥饿
- ❌ 性能一般(所有操作都synchronized)
五、版本2:公平读写锁(队列实现)⭐
核心数据结构
public class FairReadWriteLock {
// 等待队列
private final Queue<WaitNode> waitQueue = new LinkedList<>();
// 当前持有锁的线程
private final Set<Thread> readingThreads = new HashSet<>();
private Thread writingThread = null;
// 锁
private final Object lock = new Object();
// 等待节点
static class WaitNode {
Thread thread;
LockType type; // READ or WRITE
boolean notified = false;
WaitNode(Thread thread, LockType type) {
this.thread = thread;
this.type = type;
}
}
enum LockType {
READ, WRITE
}
// 读锁
public void lockRead() throws InterruptedException {
WaitNode node = new WaitNode(Thread.currentThread(), LockType.READ);
synchronized (lock) {
waitQueue.add(node);
while (!canAcquireRead(node)) {
lock.wait();
}
waitQueue.remove(node);
readingThreads.add(Thread.currentThread());
}
}
public void unlockRead() {
synchronized (lock) {
readingThreads.remove(Thread.currentThread());
lock.notifyAll();
}
}
// 写锁
public void lockWrite() throws InterruptedException {
WaitNode node = new WaitNode(Thread.currentThread(), LockType.WRITE);
synchronized (lock) {
waitQueue.add(node);
while (!canAcquireWrite(node)) {
lock.wait();
}
waitQueue.remove(node);
writingThread = Thread.currentThread();
}
}
public void unlockWrite() {
synchronized (lock) {
writingThread = null;
lock.notifyAll();
}
}
// 判断是否可以获取读锁
private boolean canAcquireRead(WaitNode node) {
// 1. 没有写线程持有锁
if (writingThread != null) {
return false;
}
// 2. 当前节点是队列头部
if (waitQueue.peek() != node) {
return false;
}
// 3. 队列中第一个是读节点(批量获取)
return true;
}
// 判断是否可以获取写锁
private boolean canAcquireWrite(WaitNode node) {
// 1. 没有任何线程持有锁
if (!readingThreads.isEmpty() || writingThread != null) {
return false;
}
// 2. 当前节点是队列头部
return waitQueue.peek() == node;
}
}
关键点:
- 队列:所有等待线程排队
- 批量获取:连续的读请求可以一起获取锁
- 公平性:严格按照队列顺序
六、版本3:优化的公平读写锁(AQS实现)🚀
使用AQS框架
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
public class OptimizedFairReadWriteLock {
private final Sync sync = new Sync();
private final ReadLock readLock = new ReadLock();
private final WriteLock writeLock = new WriteLock();
// AQS同步器
private static class Sync extends AbstractQueuedSynchronizer {
// state编码:高16位=读锁数量,低16位=写锁数量
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
static int sharedCount(int c) {
return c >>> SHARED_SHIFT;
}
static int exclusiveCount(int c) {
return c & EXCLUSIVE_MASK;
}
// 尝试获取读锁(共享模式)
@Override
protected int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
// 有写锁,失败
if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) {
return -1;
}
// 检查公平性:队列中有人在等待
if (hasQueuedPredecessors()) {
return -1;
}
int r = sharedCount(c);
if (r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {
return 1;
}
return -1;
}
// 尝试释放读锁
@Override
protected boolean tryReleaseShared(int unused) {
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc)) {
return nextc == 0;
}
}
}
// 尝试获取写锁(独占模式)
@Override
protected 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 + acquires > MAX_COUNT) {
throw new Error("Maximum lock count exceeded");
}
setState(c + acquires);
return true;
}
// 检查公平性
if (hasQueuedPredecessors() || !compareAndSetState(c, c + acquires)) {
return false;
}
setExclusiveOwnerThread(current);
return true;
}
// 尝试释放写锁
@Override
protected boolean tryRelease(int releases) {
if (Thread.currentThread() != getExclusiveOwnerThread()) {
throw new IllegalMonitorStateException();
}
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free) {
setExclusiveOwnerThread(null);
}
setState(nextc);
return free;
}
}
// 读锁
public class ReadLock {
public void lock() {
sync.acquireShared(1);
}
public void unlock() {
sync.releaseShared(1);
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
}
// 写锁
public class WriteLock {
public void lock() {
sync.acquire(1);
}
public void unlock() {
sync.release(1);
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
}
public ReadLock readLock() {
return readLock;
}
public WriteLock writeLock() {
return writeLock;
}
}
优势:
- 高性能:AQS底层优化,CAS操作
- 公平性:
hasQueuedPredecessors()检查队列 - 可重入:支持锁重入
- 可中断:支持中断等待
七、完整测试:验证公平性🧪
public class FairLockTest {
private static final OptimizedFairReadWriteLock lock =
new OptimizedFairReadWriteLock();
public static void main(String[] args) throws InterruptedException {
// 10个读线程
for (int i = 0; i < 10; i++) {
final int id = i;
new Thread(() -> {
try {
System.out.println("读线程" + id + " 请求锁");
lock.readLock().lock();
System.out.println("读线程" + id + " 获得锁");
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.readLock().unlock();
System.out.println("读线程" + id + " 释放锁");
}
}, "Reader-" + id).start();
Thread.sleep(10); // 错开启动时间
}
// 5个写线程
for (int i = 0; i < 5; i++) {
final int id = i;
new Thread(() -> {
try {
System.out.println("写线程" + id + " 请求锁");
lock.writeLock().lock();
System.out.println("写线程" + id + " 获得锁 ✍️");
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.writeLock().unlock();
System.out.println("写线程" + id + " 释放锁");
}
}, "Writer-" + id).start();
Thread.sleep(10);
}
}
}
输出(公平锁):
读线程0 请求锁
读线程0 获得锁
读线程1 请求锁
读线程1 获得锁
写线程0 请求锁
读线程2 请求锁
读线程0 释放锁
读线程1 释放锁
写线程0 获得锁 ✍️ ← 写线程获得机会
写线程0 释放锁
读线程2 获得锁
读线程2 释放锁
...
关键: 写线程0在队列中排在读线程2前面,所以先获得锁,公平!
八、性能对比测试📊
public class PerformanceTest {
private static final int THREADS = 20;
private static final int OPERATIONS = 10000;
public static void main(String[] args) throws InterruptedException {
System.out.println("=== 非公平锁 ===");
testLock(new ReentrantReadWriteLock(false));
System.out.println("\n=== 公平锁 ===");
testLock(new ReentrantReadWriteLock(true));
System.out.println("\n=== 自定义公平锁 ===");
testCustomLock();
}
private static void testLock(ReentrantReadWriteLock lock)
throws InterruptedException {
long start = System.currentTimeMillis();
Thread[] threads = new Thread[THREADS];
// 80%读,20%写
for (int i = 0; i < THREADS; i++) {
final int id = i;
threads[i] = new Thread(() -> {
for (int j = 0; j < OPERATIONS; j++) {
if (Math.random() < 0.8) {
// 读操作
lock.readLock().lock();
try {
// 模拟读
} finally {
lock.readLock().unlock();
}
} else {
// 写操作
lock.writeLock().lock();
try {
// 模拟写
} finally {
lock.writeLock().unlock();
}
}
}
});
threads[i].start();
}
for (Thread t : threads) {
t.join();
}
long time = System.currentTimeMillis() - start;
System.out.println("耗时: " + time + "ms");
}
private static void testCustomLock() throws InterruptedException {
OptimizedFairReadWriteLock lock = new OptimizedFairReadWriteLock();
long start = System.currentTimeMillis();
Thread[] threads = new Thread[THREADS];
for (int i = 0; i < THREADS; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < OPERATIONS; j++) {
if (Math.random() < 0.8) {
lock.readLock().lock();
try {
} finally {
lock.readLock().unlock();
}
} else {
lock.writeLock().lock();
try {
} finally {
lock.writeLock().unlock();
}
}
}
});
threads[i].start();
}
for (Thread t : threads) {
t.join();
}
long time = System.currentTimeMillis() - start;
System.out.println("耗时: " + time + "ms");
}
}
测试结果:
| 锁类型 | 耗时 | 吞吐量 | 公平性 |
|---|---|---|---|
| 非公平锁 | 800ms | ⭐⭐⭐⭐⭐ | ❌ |
| JDK公平锁 | 2500ms | ⭐⭐ | ✅ |
| 自定义公平锁 | 1800ms | ⭐⭐⭐ | ✅ |
结论:
- 非公平锁最快,但可能饥饿
- 公平锁慢3倍,但保证公平
- 自定义锁是折中方案
九、公平策略的变种🎨
策略1:写者优先
// 写线程永远优先于读线程
private boolean canAcquireRead() {
return writingThread == null &&
waitQueue.stream().noneMatch(n -> n.type == WRITE);
// 队列中有写请求,读线程不能获取
}
优点: 写操作不会饥饿
缺点: 读操作可能饥饿
策略2:读者优先
// 只要没有写线程持有锁,读线程就能获取
private boolean canAcquireRead() {
return writingThread == null;
// 不管队列中有没有写请求
}
优点: 读操作吞吐量高
缺点: 写操作可能饥饿(就是JDK默认行为)
策略3:混合模式
// 写线程等待超过阈值,提升优先级
private static final long WRITE_WAIT_THRESHOLD = 1000; // 1秒
private boolean canAcquireRead() {
if (writingThread != null) return false;
for (WaitNode node : waitQueue) {
if (node.type == WRITE &&
System.currentTimeMillis() - node.startTime > WRITE_WAIT_THRESHOLD) {
return false; // 写线程等太久了,让它先
}
}
return true;
}
优点: 平衡读写性能
缺点: 实现复杂
十、实战:缓存系统的读写锁🗄️
public class CachedData<K, V> {
private final Map<K, V> cache = new HashMap<>();
private final OptimizedFairReadWriteLock lock = new OptimizedFairReadWriteLock();
// 读取缓存
public V get(K key) {
lock.readLock().lock();
try {
return cache.get(key);
} finally {
lock.readLock().unlock();
}
}
// 写入缓存
public void put(K key, V value) {
lock.writeLock().lock();
try {
cache.put(key, value);
} finally {
lock.writeLock().unlock();
}
}
// 批量读取
public Map<K, V> getAll(Collection<K> keys) {
lock.readLock().lock();
try {
Map<K, V> result = new HashMap<>();
for (K key : keys) {
V value = cache.get(key);
if (value != null) {
result.put(key, value);
}
}
return result;
} finally {
lock.readLock().unlock();
}
}
// 条件更新(读后写)
public boolean putIfAbsent(K key, V value) {
// 先读后写,需要升级锁
lock.readLock().lock();
try {
if (cache.containsKey(key)) {
return false; // 已存在
}
} finally {
lock.readLock().unlock();
}
// 升级为写锁
lock.writeLock().lock();
try {
// 再次检查(可能有其他线程抢先了)
if (cache.containsKey(key)) {
return false;
}
cache.put(key, value);
return true;
} finally {
lock.writeLock().unlock();
}
}
// 清空缓存
public void clear() {
lock.writeLock().lock();
try {
cache.clear();
} finally {
lock.writeLock().unlock();
}
}
}
十一、面试高频问答💯
Q1: 公平锁和非公平锁的性能差距有多大?
A: 通常2-3倍。公平锁需要维护队列和检查顺序,开销较大。
Q2: 什么时候用公平锁?
A:
- 不能容忍饥饿的场景
- 对响应时间有严格要求
- 任务执行时间较长
Q3: 读写锁如何防止写饥饿?
A:
- 写者优先策略
- 公平模式(队列)
- 混合模式(超时提升优先级)
Q4: 读锁能升级为写锁吗?
A: 不能直接升级! 会导致死锁。必须先释放读锁,再获取写锁。
Q5: AQS的state如何编码读写锁状态?
A:
- 高16位:读锁数量
- 低16位:写锁数量(重入次数)
十二、总结:选型指南🎯
决策树
需要读写锁?
├─ 读多写少?
│ ├─ 是 → 用读写锁
│ └─ 否 → 考虑普通锁
├─ 需要公平性?
│ ├─ 是
│ │ ├─ 性能要求高 → 自定义公平锁
│ │ └─ 简单可靠 → JDK公平锁
│ └─ 否 → 非公平锁(默认)
└─ 写操作频繁?
└─ 是 → 考虑StampedLock(乐观读)
最佳实践
- 默认用非公平(性能优先)
- 防饥饿场景用公平锁
- 监控等待时间(检测饥饿)
- 避免长时间持锁(减少竞争)
- 读写分离(StampedLock乐观读)
完结撒花!🎉
我们完成了从第41题到第50题的所有知识点!这10篇文档涵盖了:
- StampedLock的乐观读
- CompletableFuture的异步编程
- Disruptor的无锁设计
- 高性能队列的实现
- happens-before原则
- wait/notify机制
- 线程中断
- CAS的ABA问题
- LongAdder的分治思想
- 公平读写锁的实现
希望这些内容能帮助你深入理解Java并发编程!💪