手写读写公平锁:公平与性能的博弈⚖️

35 阅读9分钟

公平的代价是性能,性能的代价是公平。如何在两者之间找到平衡?让我们从零开始,打造一个读写公平的锁!

一、开场:什么是公平锁?🤔

公平 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. 读读不互斥:多个读线程可以同时持有锁
  2. 读写互斥:读和写不能同时
  3. 写写互斥:写和写不能同时
  4. 公平性
    • 读线程不能"插队"到写线程前面
    • 写线程也不能一直霸占锁
  5. 防止饥饿:所有线程都能获得锁

核心思想

策略:写者优先 + 队列

队列: [读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;
    }
}

关键点:

  1. 队列:所有等待线程排队
  2. 批量获取:连续的读请求可以一起获取锁
  3. 公平性:严格按照队列顺序

六、版本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;
    }
}

优势:

  1. 高性能:AQS底层优化,CAS操作
  2. 公平性hasQueuedPredecessors()检查队列
  3. 可重入:支持锁重入
  4. 可中断:支持中断等待

七、完整测试:验证公平性🧪

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(乐观读)

最佳实践

  1. 默认用非公平(性能优先)
  2. 防饥饿场景用公平锁
  3. 监控等待时间(检测饥饿)
  4. 避免长时间持锁(减少竞争)
  5. 读写分离(StampedLock乐观读)

完结撒花!🎉

我们完成了从第41题到第50题的所有知识点!这10篇文档涵盖了:

  • StampedLock的乐观读
  • CompletableFuture的异步编程
  • Disruptor的无锁设计
  • 高性能队列的实现
  • happens-before原则
  • wait/notify机制
  • 线程中断
  • CAS的ABA问题
  • LongAdder的分治思想
  • 公平读写锁的实现

希望这些内容能帮助你深入理解Java并发编程!💪