知识模块
☕ Java 知识模块
三、Java 并发编程
读写锁

读写锁

面试高频考点:读写锁是一种优化锁机制,允许多个读操作同时进行,但写操作互斥。掌握读写锁的特性、使用场景和 StampedLock 是面试重点。

核心概念

什么是读写锁

读写锁是一种将锁分为读锁和写锁的机制:

  • 读锁(共享锁):允许多个线程同时获取,用于读取操作
  • 写锁(排他锁):同一时间只允许一个线程获取,用于写入操作
┌─────────────────────────────────────────────────────┐
│                    读写锁状态                         │
├───────────────┬─────────────────────────────────────┤
│ 读读:共享     │ 线程A读取 ══╗                        │
│               │ 线程B读取 ══╣→ 可同时进行              │
│               │ 线程C读取 ══╝                        │
├───────────────┼─────────────────────────────────────┤
│ 读写:互斥     │ 线程A读取 ══╗                        │
│               │ 线程B写入   ═╣→ 写入需等待             │
│               │            ═╝                        │
├───────────────┼─────────────────────────────────────┤
│ 写写:互斥     │ 线程A写入 ══╗                        │
│               │ 线程B写入 ══╣→ 互斥等待               │
│               │            ═╝                        │
└───────────────┴─────────────────────────────────────┘

适用场景

读写锁适用于读多写少的场景:

  • 缓存系统
  • 配置管理
  • 共享数据统计
  • 数据库查询缓存

ReentrantReadWriteLock 使用

基本用法

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
 
public class Cache<K, V> {
    private final Map<K, V> map = new HashMap<>();
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
    
    // 读操作:使用读锁
    public V get(K key) {
        rwLock.readLock().lock();
        try {
            return map.get(key);
        } finally {
            rwLock.readLock().unlock();
        }
    }
    
    // 写操作:使用写锁
    public void put(K key, V value) {
        rwLock.writeLock().lock();
        try {
            map.put(key, value);
        } finally {
            rwLock.writeLock().unlock();
        }
    }
    
    // 删除操作:使用写锁
    public V remove(K key) {
        rwLock.writeLock().lock();
        try {
            return map.remove(key);
        } finally {
            rwLock.writeLock().unlock();
        }
    }
    
    // 清空操作:使用写锁
    public void clear() {
        rwLock.writeLock().lock();
        try {
            map.clear();
        } finally {
            rwLock.writeLock().unlock();
        }
    }
}

完整缓存示例

public class ThreadSafeCache<K, V> {
    private final Map<K, V> cache = new HashMap<>();
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Lock readLock = rwLock.readLock();
    private final Lock writeLock = rwLock.writeLock();
    
    // 获取缓存
    public V get(K key) {
        readLock.lock();
        try {
            return cache.get(key);
        } finally {
            readLock.unlock();
        }
    }
    
    // 获取缓存,不存在则加载
    public V getOrLoad(K key, Function<K, V> loader) {
        // 先用读锁尝试获取
        readLock.lock();
        try {
            V value = cache.get(key);
            if (value != null) {
                return value;
            }
        } finally {
            readLock.unlock();
        }
        
        // 未命中,使用写锁加载
        writeLock.lock();
        try {
            // 双重检查,防止其他线程已加载
            V value = cache.get(key);
            if (value == null) {
                value = loader.apply(key);
                cache.put(key, value);
            }
            return value;
        } finally {
            writeLock.unlock();
        }
    }
    
    // 更新缓存
    public void put(K key, V value) {
        writeLock.lock();
        try {
            cache.put(key, value);
        } finally {
            writeLock.unlock();
        }
    }
    
    // 获取缓存大小
    public int size() {
        readLock.lock();
        try {
            return cache.size();
        } finally {
            readLock.unlock();
        }
    }
    
    // 批量获取
    public Map<K, V> getAll(Collection<K> keys) {
        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 {
            readLock.unlock();
        }
    }
}

读写锁特性

公平性选择

// 非公平锁(默认)- 吞吐量高
ReadWriteLock unfairLock = new ReentrantReadWriteLock();
 
// 公平锁 - 按请求顺序获取
ReadWriteLock fairLock = new ReentrantReadWriteLock(true);

公平锁 vs 非公平锁

特性公平锁非公平锁
获取顺序FIFO 先进先出可能插队
吞吐量较低较高
线程饥饿不会发生可能发生
适用场景对顺序要求高追求性能

可重入性

public class ReentrantExample {
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
    
    // 读锁可重入
    public void readMethod1() {
        rwLock.readLock().lock();
        try {
            System.out.println("读方法1");
            readMethod2();  // 再次获取读锁
        } finally {
            rwLock.readLock().unlock();
        }
    }
    
    public void readMethod2() {
        rwLock.readLock().lock();  // 可重入
        try {
            System.out.println("读方法2");
        } finally {
            rwLock.readLock().unlock();
        }
    }
    
    // 写锁可重入
    public void writeMethod1() {
        rwLock.writeLock().lock();
        try {
            System.out.println("写方法1");
            writeMethod2();  // 再次获取写锁
        } finally {
            rwLock.writeLock().unlock();
        }
    }
    
    public void writeMethod2() {
        rwLock.writeLock().lock();  // 可重入
        try {
            System.out.println("写方法2");
        } finally {
            rwLock.writeLock().unlock();
        }
    }
}

锁降级

锁降级:从写锁降级为读锁,允许在持有写锁时获取读锁。

public class LockDowngrade {
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
    private Object data;
    
    // 正确的锁降级
    public void processWithDowngrade() {
        rwLock.writeLock().lock();  // 获取写锁
        try {
            // 更新数据
            data = new Object();
            
            // 获取读锁(在写锁持有期间)
            rwLock.readLock().lock();
            
        } finally {
            rwLock.writeLock().unlock();  // 释放写锁,此时仍持有读锁
        }
        
        // 此时只有读锁,其他读线程可以并发访问
        try {
            // 继续读取操作
            System.out.println(data);
        } finally {
            rwLock.readLock().unlock();  // 最后释放读锁
        }
    }
}

锁升级(不支持)

// 错误示例:锁升级会导致死锁
public void wrongUpgrade() {
    rwLock.readLock().lock();  // 获取读锁
    try {
        // 尝试获取写锁 - 死锁!
        rwLock.writeLock().lock();  // 永远无法获取
    } finally {
        rwLock.readLock().unlock();
    }
}
 
// 正确做法:先释放读锁,再获取写锁
public void correctUpgrade() {
    rwLock.readLock().lock();
    try {
        // 读取检查
        if (needUpdate) {
            rwLock.readLock().unlock();  // 先释放读锁
            
            rwLock.writeLock().lock();   // 再获取写锁
            try {
                // 双重检查
                if (needUpdate) {
                    // 更新操作
                }
            } finally {
                rwLock.writeLock().unlock();
            }
            
            rwLock.readLock().lock();    // 再次获取读锁(如果需要继续读)
        }
    } finally {
        rwLock.readLock().unlock();
    }
}

StampedLock 简介

什么是 StampedLock

StampedLock 是 Java 8 引入的一种新型锁,相比 ReentrantReadWriteLock:

  • 性能更好:使用乐观读锁,读操作完全不加锁
  • 不可重入:使用时需要小心死锁
  • 返回戳记:锁的获取和释放需要配合 stamp

基本用法

import java.util.concurrent.locks.StampedLock;
 
public class StampedLockExample {
    private final StampedLock sl = new StampedLock();
    private double x, y;
    
    // 写锁
    public void move(double deltaX, double deltaY) {
        long stamp = sl.writeLock();  // 获取写锁
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            sl.unlockWrite(stamp);  // 释放写锁
        }
    }
    
    // 悲观读锁
    public double distanceFromOrigin() {
        long stamp = sl.readLock();  // 获取读锁
        try {
            return Math.sqrt(x * x + y * y);
        } finally {
            sl.unlockRead(stamp);  // 释放读锁
        }
    }
    
    // 乐观读锁
    public double distanceFromOriginOptimistic() {
        long stamp = sl.tryOptimisticRead();  // 获取乐观读戳记
        double currentX = x, currentY = y;    // 读取数据
        
        if (!sl.validate(stamp)) {  // 检查是否有写操作
            // 有写操作,乐观读失败,升级为悲观读
            stamp = sl.readLock();
            try {
                currentX = x;
                currentY = y;
            } finally {
                sl.unlockRead(stamp);
            }
        }
        
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }
}

乐观读锁

工作原理

乐观读流程:
┌──────────────────────────────────────────────────────┐
│ 1. tryOptimisticRead() 获取戳记                        │
│    ↓                                                 │
│ 2. 读取数据(不加锁,可能读到不一致数据)                │
│    ↓                                                 │
│ 3. validate(stamp) 检查戳记是否有效                    │
│    ├─ 有效 → 使用读取的数据                            │
│    └─ 无效 → 升级为悲观读锁,重新读取                   │
└──────────────────────────────────────────────────────┘

乐观读锁示例

public class OptimisticCache<K, V> {
    private final Map<K, V> cache = new HashMap<>();
    private final StampedLock sl = new StampedLock();
    
    // 乐观读获取
    public V get(K key) {
        long stamp = sl.tryOptimisticRead();  // 乐观读
        V value = cache.get(key);             // 读取数据(可能不一致)
        
        if (!sl.validate(stamp)) {            // 验证是否有效
            // 无效,升级为悲观读
            stamp = sl.readLock();
            try {
                value = cache.get(key);       // 重新读取
            } finally {
                sl.unlockRead(stamp);
            }
        }
        
        return value;
    }
    
    // 乐观读获取,带默认值
    public V getOrDefault(K key, V defaultValue) {
        long stamp = sl.tryOptimisticRead();
        V value = cache.get(key);
        
        if (!sl.validate(stamp)) {
            stamp = sl.readLock();
            try {
                value = cache.get(key);
            } finally {
                sl.unlockRead(stamp);
            }
        }
        
        return value != null ? value : defaultValue;
    }
    
    // 写操作
    public void put(K key, V value) {
        long stamp = sl.writeLock();
        try {
            cache.put(key, value);
        } finally {
            sl.unlockWrite(stamp);
        }
    }
}

锁转换

public class LockConversion {
    private final StampedLock sl = new StampedLock();
    private double value;
    
    // 乐观读转悲观读
    public void optimisticToPessimistic() {
        long stamp = sl.tryOptimisticRead();
        double v = value;
        
        if (!sl.validate(stamp)) {
            stamp = sl.readLock();  // 转换为悲观读
            try {
                v = value;
            } finally {
                sl.unlockRead(stamp);
            }
        }
    }
    
    // 读锁转写锁
    public void readToWrite() {
        long stamp = sl.readLock();
        try {
            while (true) {
                long ws = sl.tryConvertToWriteLock(stamp);  // 尝试转换
                if (ws != 0) {  // 转换成功
                    stamp = ws;
                    // 执行写操作
                    break;
                } else {  // 转换失败
                    sl.unlockRead(stamp);
                    stamp = sl.writeLock();  // 直接获取写锁
                }
            }
        } finally {
            sl.unlockWrite(stamp);
        }
    }
}

性能对比

基准测试

public class LockBenchmark {
    private static final int THREAD_COUNT = 10;
    private static final int ITERATIONS = 1000000;
    
    // ReentrantLock 性能测试
    public static void testReentrantLock() throws InterruptedException {
        ReentrantLock lock = new ReentrantLock();
        AtomicInteger counter = new AtomicInteger();
        
        long start = System.currentTimeMillis();
        
        Thread[] threads = new Thread[THREAD_COUNT];
        for (int i = 0; i < THREAD_COUNT; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < ITERATIONS; j++) {
                    lock.lock();
                    try {
                        counter.incrementAndGet();
                    } finally {
                        lock.unlock();
                    }
                }
            });
        }
        
        for (Thread t : threads) t.start();
        for (Thread t : threads) t.join();
        
        System.out.println("ReentrantLock: " + (System.currentTimeMillis() - start) + "ms");
    }
    
    // ReentrantReadWriteLock 性能测试(读多写少)
    public static void testReadWriteLock() throws InterruptedException {
        ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
        AtomicInteger counter = new AtomicInteger();
        
        long start = System.currentTimeMillis();
        
        Thread[] readers = new Thread[THREAD_COUNT - 1];
        for (int i = 0; i < readers.length; i++) {
            readers[i] = new Thread(() -> {
                for (int j = 0; j < ITERATIONS; j++) {
                    rwLock.readLock().lock();
                    try {
                        counter.get();
                    } finally {
                        rwLock.readLock().unlock();
                    }
                }
            });
        }
        
        Thread writer = new Thread(() -> {
            for (int j = 0; j < ITERATIONS / 10; j++) {  // 写操作少
                rwLock.writeLock().lock();
                try {
                    counter.incrementAndGet();
                } finally {
                    rwLock.writeLock().unlock();
                }
            }
        });
        
        for (Thread t : readers) t.start();
        writer.start();
        for (Thread t : readers) t.join();
        writer.join();
        
        System.out.println("ReadWriteLock: " + (System.currentTimeMillis() - start) + "ms");
    }
    
    // StampedLock 性能测试(乐观读)
    public static void testStampedLock() throws InterruptedException {
        StampedLock sl = new StampedLock();
        AtomicInteger counter = new AtomicInteger();
        
        long start = System.currentTimeMillis();
        
        Thread[] readers = new Thread[THREAD_COUNT - 1];
        for (int i = 0; i < readers.length; i++) {
            readers[i] = new Thread(() -> {
                for (int j = 0; j < ITERATIONS; j++) {
                    long stamp = sl.tryOptimisticRead();
                    counter.get();
                    if (!sl.validate(stamp)) {
                        stamp = sl.readLock();
                        try {
                            counter.get();
                        } finally {
                            sl.unlockRead(stamp);
                        }
                    }
                }
            });
        }
        
        Thread writer = new Thread(() -> {
            for (int j = 0; j < ITERATIONS / 10; j++) {
                long stamp = sl.writeLock();
                try {
                    counter.incrementAndGet();
                } finally {
                    sl.unlockWrite(stamp);
                }
            }
        });
        
        for (Thread t : readers) t.start();
        writer.start();
        for (Thread t : readers) t.join();
        writer.join();
        
        System.out.println("StampedLock: " + (System.currentTimeMillis() - start) + "ms");
    }
}

性能对比结果

场景ReentrantLockReentrantReadWriteLockStampedLock
纯读慢(互斥)快(共享)最快(乐观读)
纯写中等
读多写少最快
写多读少中等

实际应用示例

线程安全的计数器

public class ThreadSafeCounter {
    private final StampedLock sl = new StampedLock();
    private long value;
    
    public void increment() {
        long stamp = sl.writeLock();
        try {
            value++;
        } finally {
            sl.unlockWrite(stamp);
        }
    }
    
    public void decrement() {
        long stamp = sl.writeLock();
        try {
            value--;
        } finally {
            sl.unlockWrite(stamp);
        }
    }
    
    public long get() {
        long stamp = sl.tryOptimisticRead();
        long currentValue = value;
        if (!sl.validate(stamp)) {
            stamp = sl.readLock();
            try {
                currentValue = value;
            } finally {
                sl.unlockRead(stamp);
            }
        }
        return currentValue;
    }
    
    public long getAndReset() {
        long stamp = sl.writeLock();
        try {
            long oldValue = value;
            value = 0;
            return oldValue;
        } finally {
            sl.unlockWrite(stamp);
        }
    }
}

线程安全的 LRUCache

public class ThreadSafeLRUCache<K, V> {
    private final int capacity;
    private final LinkedHashMap<K, V> cache;
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
    
    public ThreadSafeLRUCache(int capacity) {
        this.capacity = capacity;
        this.cache = new LinkedHashMap<K, V>(capacity, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
                return size() > ThreadSafeLRUCache.this.capacity;
            }
        };
    }
    
    public V get(K key) {
        rwLock.readLock().lock();
        try {
            return cache.get(key);
        } finally {
            rwLock.readLock().unlock();
        }
    }
    
    public void put(K key, V value) {
        rwLock.writeLock().lock();
        try {
            cache.put(key, value);
        } finally {
            rwLock.writeLock().unlock();
        }
    }
    
    public V remove(K key) {
        rwLock.writeLock().lock();
        try {
            return cache.remove(key);
        } finally {
            rwLock.writeLock().unlock();
        }
    }
    
    public int size() {
        rwLock.readLock().lock();
        try {
            return cache.size();
        } finally {
            rwLock.readLock().unlock();
        }
    }
    
    public void clear() {
        rwLock.writeLock().lock();
        try {
            cache.clear();
        } finally {
            rwLock.writeLock().unlock();
        }
    }
}

面试要点

问题1

Q: 读写锁的三个状态是什么? A:

  1. 读读共享:多个读线程可以同时持有读锁
  2. 读写互斥:读锁和写锁互斥,获取写锁需要等待所有读锁释放
  3. 写写互斥:多个写线程互斥,同一时间只有一个写线程

问题2

Q: ReentrantReadWriteLock 的锁降级是什么?如何实现? A: 锁降级是指从写锁降级为读锁。实现步骤:

  1. 获取写锁
  2. 获取读锁(在写锁持有期间)
  3. 释放写锁
  4. 此时只持有读锁,其他读线程可以并发
  5. 最后释放读锁

注意:锁升级(从读锁升级为写锁)会导致死锁,不支持。

问题3

Q: StampedLock 与 ReentrantReadWriteLock 有什么区别? A:

特性ReentrantReadWriteLockStampedLock
乐观读不支持支持
可重入支持不支持
性能中等高(乐观读)
使用复杂度简单较复杂
锁转换不支持支持

问题4

Q: 什么是乐观读锁?有什么优势? A: 乐观读锁是一种不加锁的读取方式:

  • 工作原理:读取数据后验证是否有写操作,无则使用,有则升级为悲观读
  • 优势:读操作完全不加锁,性能最高
  • 适用场景:读多写少、读操作频繁且冲突概率低

问题5

Q: 读写锁适用于什么场景? A: 适用于读多写少的场景:

  1. 缓存系统:读频繁,更新少
  2. 配置管理:读取频繁,修改少
  3. 统计信息:频繁查询,偶尔更新
  4. 数据库查询缓存

不适合:写多读少的场景,反而会降低性能。

问题6

Q: StampedLock 为什么不可重入?有什么风险? A: StampedLock 设计上不可重入,是为了提高性能和简化实现。

风险

// 错误:会导致死锁
public void method1() {
    long stamp = sl.readLock();
    try {
        method2();  // 再次尝试获取锁会死锁
    } finally {
        sl.unlockRead(stamp);
    }
}
 
public void method2() {
    long stamp = sl.readLock();  // 死锁!
    // ...
}

解决方案:避免在持有锁时调用其他需要相同锁的方法。

总结

锁类型特点适用场景
ReentrantLock简单互斥写多、竞争激烈
ReentrantReadWriteLock读写分离读多写少
StampedLock乐观读读极多写极少

最佳实践

  1. 根据读写比例选择合适的锁
  2. 写操作用写锁,读操作用读锁
  3. StampedLock 注意不可重入
  4. 注意锁的释放顺序
  5. 高并发场景优先考虑 StampedLock 乐观读

读写锁是高并发场景下的重要优化手段!