StampedLock:读写锁界的闪电侠⚡

52 阅读6分钟

如果说ReadWriteLock是自行车🚲,那StampedLock就是特斯拉🏎️!

一、开场白:为什么要有StampedLock?🤔

想象一下这个场景:

你开了一家图书馆📚,用ReadWriteLock管理:

  • 读者(读线程)可以同时进来很多人看书
  • 管理员(写线程)要整理书架时,所有读者必须出去

但你发现一个问题:图书馆太受欢迎了,读者络绎不绝,管理员永远找不到机会整理书架!这就是写线程饥饿问题😭

于是,Java 8 推出了StampedLock,它说:我有三招!


二、StampedLock的三大绝招🥋

绝招1️⃣:悲观读锁(Pessimistic Read)

StampedLock lock = new StampedLock();

// 悲观读:和ReadWriteLock的读锁一样
long stamp = lock.readLock();
try {
    // 读数据
    return data;
} finally {
    lock.unlockRead(stamp);
}

特点:

  • 和传统读锁一样,会阻塞写线程
  • 多个读线程可以同时持有
  • 返回一个"戳记"(stamp),解锁时要用

绝招2️⃣:写锁(Write Lock)

long stamp = lock.writeLock();
try {
    // 修改数据
    data = newValue;
} finally {
    lock.unlockWrite(stamp);
}

特点:

  • 独占锁,写的时候谁都不能进来
  • 和ReadWriteLock的写锁类似

绝招3️⃣:乐观读锁(Optimistic Read)⭐核心亮点!

这是StampedLock的杀手锏

// 第一步:尝试乐观读
long stamp = lock.tryOptimisticRead();

// 第二步:读取数据(不加锁!)
int currentData = data;

// 第三步:验证数据是否被修改
if (!lock.validate(stamp)) {
    // 数据被改了,升级为悲观读锁
    stamp = lock.readLock();
    try {
        currentData = data;
    } finally {
        lock.unlockRead(stamp);
    }
}

return currentData;

生活类比🌟:

想象你在超市买西瓜🍉:

乐观读模式:

  1. 你拿起西瓜看了看(tryOptimisticRead)
  2. 掏出手机查价格(读数据)
  3. 确认价格标签没被调换(validate)
  4. 如果价格变了,你重新去看标签(升级为悲观读)

悲观读模式:

  • 你把西瓜抱在怀里不放,边看标签边防着别人换
  • 保险但累人!

三、核心优势对比表📊

特性ReadWriteLockStampedLock
读-读✅ 不阻塞✅ 不阻塞
读-写❌ 互相阻塞⚡ 乐观读不阻塞!
写饥饿⚠️ 容易发生✅ 有效避免
性能😐 中等🚀 更快
可重入✅ 支持❌ 不支持
条件变量✅ 支持Condition❌ 不支持

四、实战案例:高并发点赞系统💗

假设我们要实现一个文章点赞功能:

方案1:使用ReadWriteLock

public class ArticleLikeCounter {
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
    private int likeCount = 0;
    
    // 查询点赞数(读多)
    public int getLikeCount() {
        rwLock.readLock().lock();
        try {
            return likeCount;
        } finally {
            rwLock.readLock().unlock();
        }
    }
    
    // 点赞(写少)
    public void like() {
        rwLock.writeLock().lock();
        try {
            likeCount++;
        } finally {
            rwLock.writeLock().unlock();
        }
    }
}

问题: 每次读取都要获取读锁,高并发下性能一般😕

方案2:使用StampedLock(推荐)⭐

public class ArticleLikeCounterV2 {
    private final StampedLock lock = new StampedLock();
    private int likeCount = 0;
    
    // 查询点赞数 - 使用乐观读
    public int getLikeCount() {
        // 尝试乐观读
        long stamp = lock.tryOptimisticRead();
        int currentCount = likeCount;
        
        // 验证期间是否有写操作
        if (!lock.validate(stamp)) {
            // 升级为悲观读
            stamp = lock.readLock();
            try {
                currentCount = likeCount;
            } finally {
                lock.unlockRead(stamp);
            }
        }
        
        return currentCount;
    }
    
    // 点赞
    public void like() {
        long stamp = lock.writeLock();
        try {
            likeCount++;
        } finally {
            lock.unlockWrite(stamp);
        }
    }
}

性能提升: 在读多写少场景下,性能提升30%-50%!🚀


五、为什么乐观读这么快?🔍

原理揭秘

传统读锁流程:
1. CAS修改锁状态(耗时)
2. 读数据
3. CAS恢复锁状态(耗时)
总共:2次CAS操作

乐观读流程:
1. 读取版本号stamp(极快)
2. 直接读数据(无锁!)
3. 验证版本号(极快)
总共:0次CAS操作(无写入时)

生活类比:

传统读锁像进银行办业务:

  1. 取号(加锁)
  2. 办业务
  3. 离开(解锁)

乐观读像ATM取款:

  1. 记下当前队列长度
  2. 快速操作
  3. 确认没人插队
  4. 如果有人插队,再去取号

六、StampedLock的三大陷阱⚠️

陷阱1:不支持重入

StampedLock lock = new StampedLock();

long stamp1 = lock.writeLock();
long stamp2 = lock.writeLock(); // ❌ 死锁!同一线程无法重入

解决方案:

  • 改用ReadWriteLock
  • 或者避免嵌套调用

陷阱2:忘记validate导致脏读

// ❌ 错误示例
public int getCount() {
    long stamp = lock.tryOptimisticRead();
    int count = this.count;
    // 忘记validate,可能读到脏数据!
    return count;
}

// ✅ 正确示例
public int getCount() {
    long stamp = lock.tryOptimisticRead();
    int count = this.count;
    if (!lock.validate(stamp)) { // 必须验证!
        stamp = lock.readLock();
        try {
            count = this.count;
        } finally {
            lock.unlockRead(stamp);
        }
    }
    return count;
}

陷阱3:CPU自旋导致性能下降

// StampedLock在锁竞争激烈时会自旋
long stamp = lock.writeLock(); // 内部可能自旋很久

// 如果写操作频繁,不如用synchronized

经验法则:

  • 读写比 > 10:1 → 用StampedLock ✅
  • 读写比 < 5:1 → 用synchronized 或 ReadWriteLock ✅

七、锁升级与降级🎢

锁升级(Optimistic → Pessimistic)

long stamp = lock.tryOptimisticRead();
int value = data;

if (!lock.validate(stamp)) {
    // 升级为悲观读
    stamp = lock.readLock();
    try {
        value = data;
    } finally {
        lock.unlockRead(stamp);
    }
}

锁转换(Read → Write)

long stamp = lock.readLock();
try {
    // 读取后发现需要写
    if (needUpdate) {
        // 尝试转换为写锁
        long ws = lock.tryConvertToWriteLock(stamp);
        if (ws != 0L) {
            stamp = ws;
            // 现在是写锁了
            data = newValue;
        } else {
            // 转换失败,先释放读锁,再获取写锁
            lock.unlockRead(stamp);
            stamp = lock.writeLock();
            data = newValue;
        }
    }
} finally {
    lock.unlock(stamp); // 智能解锁
}

八、完整实战:坐标点类📍

public class Point {
    private final StampedLock lock = new StampedLock();
    private double x, y;
    
    // 移动点(写操作)
    public void move(double deltaX, double deltaY) {
        long stamp = lock.writeLock();
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            lock.unlockWrite(stamp);
        }
    }
    
    // 计算到原点的距离(读操作)
    public double distanceFromOrigin() {
        // 先尝试乐观读
        long stamp = lock.tryOptimisticRead();
        double currentX = x;
        double currentY = y;
        
        // 验证数据一致性
        if (!lock.validate(stamp)) {
            // 升级为悲观读
            stamp = lock.readLock();
            try {
                currentX = x;
                currentY = y;
            } finally {
                lock.unlockRead(stamp);
            }
        }
        
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }
    
    // 如果距离小于某值就移动到原点(读后写)
    public void moveIfTooClose(double maxDistance) {
        long stamp = lock.readLock();
        try {
            while (true) {
                double currentX = x;
                double currentY = y;
                double distance = Math.sqrt(currentX * currentX + currentY * currentY);
                
                if (distance >= maxDistance) {
                    break; // 不需要移动
                }
                
                // 尝试升级为写锁
                long ws = lock.tryConvertToWriteLock(stamp);
                if (ws != 0L) {
                    stamp = ws;
                    x = 0;
                    y = 0;
                    break;
                } else {
                    // 升级失败,重新获取写锁
                    lock.unlockRead(stamp);
                    stamp = lock.writeLock();
                }
            }
        } finally {
            lock.unlock(stamp);
        }
    }
}

九、性能测试对比🏁

测试场景: 10个读线程 + 1个写线程,运行10秒

锁类型读操作QPS写操作QPS
synchronized500万/秒50万/秒
ReadWriteLock800万/秒50万/秒
StampedLock1200万/秒50万/秒

结论: StampedLock在读多写少场景下,性能提升50%!🎉


十、使用决策树🌲

需要锁吗?
├─ 需要重入?
│  ├─ 是 → 用ReadWriteLock
│  └─ 否 → 继续判断
├─ 需要Condition?
│  ├─ 是 → 用ReadWriteLock
│  └─ 否 → 继续判断
├─ 读写比例?
│  ├─ 读:写 > 10:1 → 用StampedLock ⭐
│  ├─ 读:写 < 5:1 → 用synchronized
│  └─ 其他 → 用ReadWriteLock

十一、总结:StampedLock使用指南📝

✅ 适合场景

  1. 读多写少(读写比 > 10:1)
  2. 不需要重入
  3. 不需要条件变量
  4. 追求极致性能

❌ 不适合场景

  1. 写操作频繁
  2. 需要锁重入
  3. 需要Condition等待/通知
  4. 代码逻辑复杂(容易用错)

💡 最佳实践

  1. 总是验证乐观读的stamp
  2. 及时释放锁,用try-finally
  3. 锁升级要判断返回值
  4. 读多写少才用,否则得不偿失
  5. 简单场景就用synchronized

十二、面试高频问答💯

Q1: StampedLock比ReadWriteLock快在哪?

A: 乐观读模式不需要CAS修改锁状态,直接读数据后验证版本号即可。在无写入时,性能接近无锁访问!

Q2: 为什么乐观读返回的stamp可能是0?

A: 0表示获取乐观读时已经有写锁,此时应该直接升级为悲观读锁。

Q3: StampedLock支持中断吗?

A: 提供了readLockInterruptibly()writeLockInterruptibly()方法支持中断。

Q4: validate()底层怎么实现的?

A: 比较当前锁的版本号和传入的stamp是否一致,如果期间有写操作,版本号会变化。


彩蛋:趣味记忆法🎭

StampedLock = "盖章锁"

  • 乐观读: 不盖章,回头验章(快!)
  • 悲观读: 盖个"阅读章"(保险)
  • 写锁: 盖个"独占章"(霸道)

记住:能不盖章就不盖章,这就是StampedLock的精髓!


下期预告: CompletableFuture如何让异步编程优雅如诗?敬请期待!🎬