第6章

🔒 读写锁

掌握读写分离的高效并发控制机制,理解ReentrantReadWriteLock和StampedLock的原理与应用

学习目标

读写锁概述

读写锁(ReadWriteLock)是一种特殊的锁机制,它允许多个线程同时获取读锁,但只允许一个线程获取写锁。这种设计基于一个重要的观察:在很多应用场景中,读操作远比写操作频繁,而多个读操作之间并不会产生数据竞争。

核心思想

读写锁通过读写分离的策略,在保证数据一致性的前提下,显著提高了并发读取的性能。

读写锁的特性

共享读锁
多个线程可以同时获取读锁,实现并发读取,提高系统吞吐量。
独占写锁
只有一个线程可以获取写锁,确保写操作的原子性和数据一致性。
互斥性
读锁和写锁之间是互斥的,写锁和写锁之间也是互斥的。

适用场景

ReentrantReadWriteLock详解

ReentrantReadWriteLock是Java中ReadWriteLock接口的标准实现,它提供了可重入的读写锁功能。该类内部维护了两个锁:一个读锁和一个写锁,它们共享同一个同步状态。

基本用法

ReentrantReadWriteLock基本示例
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockExample {
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private final ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
    private final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
    
    private String data = "初始数据";
    
    // 读操作
    public String readData() {
        readLock.lock();
        try {
            // 模拟读取操作
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + " 读取数据: " + data);
            return data;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return null;
        } finally {
            readLock.unlock();
        }
    }
    
    // 写操作
    public void writeData(String newData) {
        writeLock.lock();
        try {
            // 模拟写入操作
            Thread.sleep(2000);
            this.data = newData;
            System.out.println(Thread.currentThread().getName() + " 写入数据: " + data);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            writeLock.unlock();
        }
    }
}

锁的特性

可重入性
同一个线程可以多次获取同一个锁,避免死锁问题。
公平性
支持公平和非公平两种模式,可在构造时指定。
超时获取
支持tryLock()方法,可设置超时时间避免无限等待。

锁的升级和降级

ReentrantReadWriteLock支持锁降级但不支持锁升级。锁降级是指持有写锁的线程可以获取读锁,然后释放写锁,从而将写锁降级为读锁。

重要提醒

ReentrantReadWriteLock不支持锁升级(从读锁升级到写锁),这样设计是为了避免死锁。

锁降级示例

锁降级实现
public class LockDowngradeExample {
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private final ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
    private final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
    
    private volatile boolean dataChanged = false;
    private String data = "初始数据";
    
    public void processData() {
        readLock.lock();
        try {
            if (!dataChanged) {
                // 需要更新数据,先释放读锁
                readLock.unlock();
                
                // 获取写锁
                writeLock.lock();
                try {
                    // 双重检查
                    if (!dataChanged) {
                        data = "更新后的数据";
                        dataChanged = true;
                    }
                    
                    // 锁降级:在释放写锁之前获取读锁
                    readLock.lock();
                } finally {
                    writeLock.unlock(); // 释放写锁
                }
            }
            
            // 使用数据(持有读锁)
            System.out.println("处理数据: " + data);
            
        } finally {
            readLock.unlock();
        }
    }
}

锁降级的优势

StampedLock高级特性

StampedLock是Java 8引入的新锁机制,它提供了三种锁模式:写锁、悲观读锁和乐观读。相比ReentrantReadWriteLock,StampedLock在读多写少的场景下性能更优。

三种锁模式

写锁
独占锁,与ReentrantReadWriteLock的写锁类似,但不可重入。
悲观读锁
共享锁,与ReentrantReadWriteLock的读锁类似,但不可重入。
乐观读
无锁操作,通过版本号检查数据是否被修改,性能最优。

StampedLock使用示例

StampedLock完整示例
import java.util.concurrent.locks.StampedLock;

public class StampedLockExample {
    private final StampedLock lock = new StampedLock();
    private double x, y;
    
    // 写操作
    public void write(double newX, double newY) {
        long stamp = lock.writeLock();
        try {
            x = newX;
            y = newY;
        } finally {
            lock.unlockWrite(stamp);
        }
    }
    
    // 乐观读
    public double distanceFromOrigin() {
        long stamp = lock.tryOptimisticRead();
        double curX = x, curY = y;
        
        // 检查在读取过程中是否有写操作
        if (!lock.validate(stamp)) {
            // 乐观读失败,降级为悲观读
            stamp = lock.readLock();
            try {
                curX = x;
                curY = y;
            } finally {
                lock.unlockRead(stamp);
            }
        }
        
        return Math.sqrt(curX * curX + curY * curY);
    }
    
    // 悲观读
    public double[] getCurrentPoint() {
        long stamp = lock.readLock();
        try {
            return new double[]{x, y};
        } finally {
            lock.unlockRead(stamp);
        }
    }
    
    // 读锁升级为写锁
    public void moveIfAtOrigin(double newX, double newY) {
        long stamp = lock.readLock();
        try {
            while (x == 0.0 && y == 0.0) {
                // 尝试将读锁升级为写锁
                long writeStamp = lock.tryConvertToWriteLock(stamp);
                if (writeStamp != 0L) {
                    stamp = writeStamp;
                    x = newX;
                    y = newY;
                    break;
                } else {
                    // 升级失败,释放读锁,获取写锁
                    lock.unlockRead(stamp);
                    stamp = lock.writeLock();
                }
            }
        } finally {
            lock.unlock(stamp);
        }
    }
}

StampedLock的优势

性能分析与最佳实践

选择合适的读写锁实现对系统性能有重要影响。不同的锁机制在不同场景下表现各异,需要根据具体的应用特点进行选择。

性能对比

读多写少场景
StampedLock > ReentrantReadWriteLock > synchronized
读写均衡场景
ReentrantReadWriteLock ≈ StampedLock > synchronized
写多读少场景
synchronized ≈ ReentrantReadWriteLock ≈ StampedLock

最佳实践

使用建议
  • 场景选择:读多写少场景优先考虑StampedLock
  • 可重入需求:需要可重入特性时选择ReentrantReadWriteLock
  • 公平性要求:需要公平锁时使用ReentrantReadWriteLock
  • 简单场景:简单的互斥场景可以使用synchronized

注意事项

上一章:Lock接口 返回目录 下一章:Condition条件