Java 线程与锁:JLS 第 17 章核心机制解析
在 Java 并发编程中,线程和锁是最基础也最重要的概念。语言规范(JLS)虽然描述严谨,但其中蕴含的知识和最佳实践值得深入研读。本文基于 Oracle《The Java Language Specification, Java SE 8 Edition》第 17 章《Threads and Locks》,结合个人理解进行解读,旨在帮助开发者透彻理解多线程环境下的内存可见性与同步语义。
注意:本文讨论的是 Java 语言规范,而非 JVM 规范。JVM 实现需满足语言规范定义的内容,但具体细节由厂商决定。语言规范追求严谨全面,同时保留足够的优化空间。
建议分三部分阅读:
- 17.1、17.2、17.3:涵盖 wait、notify、中断等线程通信机制;
- 17.4:内存模型,重点在于重排序和 happens-before 关系;
- 剩余部分:final 语义、字分裂、double/long 非原子问题等独立主题。
Chapter 17. Threads and Locks
前言
在 Java 中,线程由 Thread 类表示,用户创建线程的唯一方式是创建 Thread 实例。在相应实例上调用 start() 方法将启动线程。
若未正确使用同步,多线程程序的表现往往令人困惑且违反直觉。本章描述多线程编程的语义规则,定义了在多线程环境中线程对共享内存中值的修改是否对其他线程立即可见。
Java 编程语言内存模型(JMM)屏蔽了不同硬件架构的差异。简单来说,开发者不再关心 CPU 核心与主内存的物理交互,而是关注:每个线程拥有自己的工作内存,所有线程共享主内存。
这些语义不规定 JVM 的具体执行策略,而是限定行为规则。无论 JVM 如何优化,表现出的行为必须符合规范。
17.1 同步(Synchronization)
Java 提供多种线程通信机制,最基础的是使用 synchronized 关键字配合监视器(Monitor)实现。Java 中的每个对象都关联了一个监视器,线程可对其进行加锁和解锁操作。
同一时间,只有一个线程能获取对象上的监视器锁。其他线程若试图获取被占用的锁,将被阻塞直至成功。监视器锁支持重入:若线程 t 已持有锁,可在解锁前重复获取;每次解锁反转一次加锁效果。
synchronized 有两种使用方式:
- synchronized 代码块:
synchronized(object)尝试在指定对象的监视器上加锁。获取成功后执行代码块,执行完成或抛出异常时自动解锁。 - synchronized 修饰方法:
- 实例方法:锁定该实例对象的监视器。
- 静态方法:锁定 Class 对象的监视器。
- 方法体执行完成或异常退出后,自动解锁。
Java 语言规范不要求阻止或检测死锁。若需在多个对象上加锁,应使用传统方法避免死锁,必要时创建更高层级的无死锁原语。
此外,Java 还提供 volatile 变量读写、java.util.concurrent 包中的同步工具类等机制。
同步关键点:synchronized 的锁基于 Java 对象的监视器 Monitor,任何对象均可作为锁。面试常问:类中两个 synchronized static 方法之间是否构成同步?答案是构成同步,因为它们锁住的是同一个 Class 对象。
17.2 等待集合和唤醒(Wait Sets and Notification)
每个 Java 对象关联一个监视器和一个等待集合。等待集合是线程集合,向其中添加或移除线程的操作是原子的。相关操作包括 Object.wait、Object.notify、Object.notifyAll。
等待集合受线程中断状态影响。sleep 和 join 方法也能感知 wait 和 notify。
本节重点讲解 Thread 中的 sleep、join、interrupt,以及继承自 Object 的 wait、notify、notifyAll。
17.2.1 等待(Wait)
等待操作由 wait()、wait(long millisecs)、wait(long millisecs, int nanosecs) 引发。参数为 0 时等效于 。


