登录
首页 >  文章 >  java教程

Java多线程加锁方式全解析

时间:2026-05-29 17:41:36 114浏览 收藏

Java多线程加锁并非“越新越强”,而需紧扣实际需求精准选型:synchronized简洁安全,适合简单同步场景,但易因锁对象不当引发空指针、死锁或性能瓶颈;ReentrantLock赋予中断响应、超时获取和公平策略等高级能力,却要求开发者严守lock/unlock配对,稍有疏忽便导致锁泄漏;StampedLock则专为读多写少、可容忍瞬时脏读的高性能场景设计,以乐观读机制大幅提升并发吞吐,但用法迥异、不支持重入且校验逻辑极易遗漏。真正关键的不是语法炫技,而是清醒判断——是否需要中断?能否接受短暂不一致?写操作是否频繁?再叠加锁对象生命周期与作用域的严谨管控,才能让并发控制既高效又可靠。

Java多线程加锁的三种方式_Java线程同步加锁方案对比

synchronized 关键字:最直接但容易误用

Java 里最基础的加锁方式,编译器自动处理锁的获取和释放,适合简单同步场景。但它锁的是对象监视器(monitor),不是代码块本身,这点常被忽略。

常见错误现象:NullPointerException(锁对象为 null)、死锁(多个 synchronized 方法交叉调用且锁顺序不一致)、锁粒度太大导致吞吐量骤降。

  • 实例方法上加 synchronized,锁的是当前实例(this);静态方法上加,锁的是类对象(MyClass.class
  • 同步代码块推荐写成 synchronized(obj) { ... },且 obj 必须是不可变、非空、生命周期可控的对象(避免用 String 或装箱类型如 Integer 作锁)
  • 不能中断等待锁的线程,也不能超时尝试,一旦阻塞就只能等

ReentrantLock:可中断、可超时、可公平,但必须手动释放

ReentrantLock 提供了比 synchronized 更细粒度的控制能力,但代价是必须显式调用 lock()unlock(),漏掉 unlock() 就会造成永久阻塞。

使用场景:需要响应中断(比如线程被 interrupt() 时及时退出)、需要尝试获取锁(tryLock())、或需指定公平策略(new ReentrantLock(true))。

  • 务必在 finally 块中调用 unlock(),否则极易发生“锁泄漏”
  • lockInterruptibly() 可被中断,而 lock() 不会响应中断信号
  • 支持条件队列(Condition),比 wait()/notify() 更灵活,但一个 ReentrantLock 可绑定多个 Condition

StampedLock:读多写少场景下的高性能替代,但不支持重入

StampedLock 是 JDK 8 引入的乐观读锁机制,适用于读操作远多于写操作、且对写一致性要求不极端严格的场景。它不基于 monitor,也不支持重入,用法和前两者完全不同。

容易踩的坑:把 tryOptimisticRead() 返回的 stamp 当作“锁成功”,其实它只是个版本戳;未校验 stamp 是否有效就使用数据,会导致读到脏值。

  • 乐观读失败后要退回到悲观读(readLock()),或写锁(writeLock()
  • 每次读取共享变量前,必须调用 validate(stamp) 判断是否被其他线程修改过
  • 不支持条件等待,不能替代 wait/notifyCondition
  • 写锁是独占的,但读锁之间不互斥,也和乐观读不互斥——这是性能优势来源,也是理解难点

选哪个?关键看三件事

不是越新越好,也不是越灵活越合适。决定因素其实是:是否需要中断、是否允许脏读、是否频繁写入。

  • 只保护几行简单逻辑,无中断/超时需求 → 用 synchronized,够用且不易出错
  • 需要 tryLock(timeout)、要响应中断、或需多个等待队列 → 上 ReentrantLock,但得盯紧 finally 里的 unlock()
  • 读操作占比 >95%,能容忍极短时间内的不一致(比如缓存刷新延迟),且确定不会递归调用读逻辑 → 才考虑 StampedLock,否则反而增加复杂度和风险

最容易被忽略的一点:锁对象的作用域。无论哪种方式,锁对象如果被外部修改或复用,都会让同步失效。别让锁变成“形同虚设”的全局变量。

到这里,我们也就讲完了《Java多线程加锁方式全解析》的内容了。个人认为,基础知识的学习和巩固,是为了更好的将其运用到项目中,欢迎关注golang学习网公众号,带你了解更多关于的知识点!

资料下载
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>