Java 锁完全指南:从 synchronized 到 StampedLock

举报
PikeTalk 发表于 2025/12/23 13:45:33 2025/12/23
【摘要】 如果你问一个初级开发者:“Java 中怎么保证线程安全?”他大概率会回答你:用 synchronized 关键字。这没错——synchronized 简单、安全,在很多基础场景下确实够用。但当你成长为高级工程师,开始设计高吞吐、低延迟的系统时,你会发现:“简单”往往意味着“性能瓶颈”。在高并发架构的世界里,没有万能的锁。今天,我们就深入 java.util.concurrent.locks ...

如果你问一个初级开发者:“Java 中怎么保证线程安全?”
他大概率会回答你:用 synchronized 关键字。

这没错——synchronized 简单、安全,在很多基础场景下确实够用。

但当你成长为高级工程师,开始设计高吞吐、低延迟的系统时,你会发现:
“简单”往往意味着“性能瓶颈”。

在高并发架构的世界里,没有万能的锁
今天,我们就深入 java.util.concurrent.locks 包,一起认识 Java 锁家族的三大主力:

  • ReentrantLock
  • ReentrantReadWriteLock
  • 以及性能怪兽 StampedLock

1. ReentrantLock:手动挡的“精准控制”

你可以把 synchronized 想象成一辆自动挡汽车:
它自动帮你加减档(自动加锁/解锁),但你完全没法干预过程。

ReentrantLock 就像手动挡——
你拥有完全控制权,但一旦忘了换挡(忘记解锁),车子就会熄火(死锁)

为什么需要它?

核心原因是:灵活性
相比 synchronizedReentrantLock 提供了几个“超能力”:

  • 可中断:等待锁的线程可以被中断(lock.lockInterruptibly()
  • 带超时:尝试获取锁,2 秒拿不到就放弃(lock.tryLock(timeout)
  • 公平性:可以强制按“先来后到”顺序分配锁(FIFO,避免线程饿死)

“可重入”是什么意思?

“Reentrant”(可重入)的意思是:
如果一个线程已经持有这把锁,它可以再次获取同一把锁而不被阻塞
这对递归调用或类内部方法互相调用非常关键。

架构师的最佳实践

永远用 try-finally 块! 这在代码评审中是铁律。

Lock lock = new ReentrantLock();

lock.lock();
try {
    // 临界区
    processData();
} finally {
    // 如果这里不 unlock,服务器迟早会卡死!
    lock.unlock();
}

2. ReentrantReadWriteLock:为“读多写少”而生

想象一个商品目录 API:
每分钟有 1 万名用户在查价格,但管理员每天只更新一次。

如果用 ReentrantLock,那每次读操作都会互相阻塞——
线程 A 在读,线程 B 就得等。
读操作不会破坏数据,这种阻塞完全是浪费资源!

这时候,ReentrantReadWriteLock 就派上用场了。

它是怎么工作的?

它把锁拆成两种:

  • 读锁(Read Lock):共享的,多个线程可以同时持有
  • 写锁(Write Lock):独占的,只有一个人能写,且写的时候不能有人读

白板类比(很好理解!)

想象教室里的白板:

  • 读锁:老师走开了,30 个学生可以同时看白板
  • 写锁:老师站在白板前写字,没人能看(读),也没人能插手写(写)

什么时候用?

当你的读写比例严重失衡(比如 90% 读,10% 写)时,果断上它!

ReadWriteLock rwLock = new ReentrantReadWriteLock();

// 多个线程可以同时进入!
public String getPrice() {
    rwLock.readLock().lock();
    try {
        return this.price;
    } finally {
        rwLock.readLock().unlock();
    }
}

// 只有一个线程能进入
public void setPrice(String newPrice) {
    rwLock.writeLock().lock();
    try {
        this.price = newPrice;
    } finally {
        rwLock.writeLock().unlock();
    }
}

3. StampedLock:F1 赛车级别的性能怪兽

StampedLock 是 Java 8 引入的“性能猛兽”,专为极致吞吐设计。

ReadWriteLock 的痛点

即使是 ReentrantReadWriteLock,也有开销:
每次读者获取锁,都要更新内存中的“读者计数器”。
如果成百上千个线程同时争抢这个计数器,就会产生内存竞争(memory contention),反而拖慢性能。

StampedLock 的绝招:乐观读(Optimistic Reading)

它说:“我不急着加锁。我先直接读数据,读完再检查一个‘时间戳’(stamp),看看期间有没有人改过数据。”

  • 如果没人改?太好了!你刚刚完成了一次零开销的读操作
  • 如果有人改了?没关系,退一步,老老实实用读锁重读一遍。

⚠️ 重要警告(务必注意!)

StampedLock 不是可重入的
如果你在一个方法里拿了 StampedLock,又调用了另一个也试图拿同一把锁的方法,会自己把自己锁死(死锁)
这一点和前面两种锁完全不同!

代码示例(虽然啰嗦,但值得)

StampedLock sl = new StampedLock();

double x, y;

public double optimisticRead() {
    // 1. 获取一个“乐观读”的 stamp(实际上没加锁!)
    long stamp = sl.tryOptimisticRead();
    
    // 2. 把状态拷贝到局部变量
    double currentX = x;
    double currentY = y;

    // 3. 验证 stamp:期间有没有人写过?
    if (!sl.validate(stamp)) {
        // 4. 如果验证失败(数据被改了),那就老老实实用读锁
        stamp = sl.readLock();
        try {
            currentX = x;
            currentY = y;
        } finally {
            sl.unlockRead(stamp);
        }
    }
    
    return Math.sqrt(currentX * currentX + currentY * currentY);
}

总结:到底该选哪种锁?

作为架构师,我给团队的“速查表”如下:

场景 推荐锁
简单同步,低并发 synchronized
需要中断、超时、公平性 ReentrantLock
读远多于写(如缓存、配置) ReentrantReadWriteLock
极致性能,读极频繁,能接受复杂逻辑 StampedLock

💡 小贴士StampedLock 虽快,但代码难读、易出错。永远从最简单的方案开始
先用性能分析工具(profiler)确认锁确实是瓶颈,再考虑升级到复杂锁。


最后的话

并发是一把双刃剑。
StampedLock 虽快,但用不好反而会引入 bug 或死锁。

不要为了炫技而用高级锁,要为真实性能问题而用。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。