JUC常见核心知识梳理01 线程 并发 JMM volatile 管程 锁 synchronized

一 基础概念:进程、线程、并发、并行

概念

概念 本质 记忆点
进程 程序的一次执行实例,资源分配的基本单位 有独立内存空间
线程 CPU 调度的基本单位,进程内的执行路径 一个进程可含多个线程
并发(concurrent) 单核下时间片轮转,宏观同时、微观交替 交替执行
并行(parallel) 多核下真正同时执行 同时执行
上下文切换 保存/恢复程序计数器、栈帧等现场 代价高,是"线程越多越慢"的根因

线程共享 vs 私有

  • 共享:堆、方法区(元空间)
  • 私有:程序计数器、虚拟机栈、本地方法栈

这条决定了局部变量天然线程安全 (栈封闭),而成员变量/静态变量才有线程安全问题

三个性能事实

  1. 只有多核才有意义 :讲义实测,双核 CPU 下 4 线程并行求和比单线程快约一倍 (0.020 s/op vs 0.043 s/op);单核下两者几乎一致(0.061 vs 0.064)------单核多线程只是徒增上下文切换
  2. 线程数不是越多越好:超过 CPU 核数后,上下文切换开销开始反噬吞吐量。
  3. 栈与栈帧 :每个线程有独立栈,方法调用压栈帧、结束出栈;-Xss 调栈大小,过小会 StackOverflowError

二 线程基础:创建、API、状态流转

四种创建方式

方式 特点 备注
继承 Thread 重写 run() 最简单 单继承限制,不推荐
实现 Runnable + new Thread(r) 推荐:任务与线程解耦、可共享任务对象 @FunctionalInterface,可用 lambda 简化
Callable + FutureTask 有返回值、可抛受检异常 future.get() 阻塞获取
线程池 execute/submit 生产唯一推荐 复用线程,见第八章
java 复制代码
// Callable + FutureTask
FutureTask<Integer> task = new FutureTask<>(() -> { return 1; });
new Thread(task, "t1").start();
Integer r = task.get();   // 阻塞等待结果,内部即"保护性暂停"模式

高频 API 语义

方法 本质 易错点
start() 新建线程并异步执行 run() 一个 Thread 对象只能 start 一次 ,重复抛 IllegalThreadStateException
run() 普通方法调用 直接调 run() 不会开新线程
sleep(ms) 当前线程休眠 → TIMED_WAITING 不释放锁 ;是 Thread 静态方法;sleep 写在哪个线程里就休眠哪个线程
yield() 让出 CPU → 就绪态 只是"提示",任务调度器可完全无视;与 sleep 区别见下
join() / join(n) 等待目标线程结束 底层是 wait/notify(保护性暂停);join(n) 取**"实际等待时间"与"设定时间"的较小值**
interrupt() 设置打断标记 打断 sleep/wait/join 会抛 InterruptedException 并清空标记
isInterrupted() 读标记,不清除 vs Thread.interrupted()(静态方法,会清除
setPriority(1~10) 默认 5,数字越大优先级越高 只影响调度概率,调度器权力大于优先级
setDaemon(true) 守护线程 JVM 退出不管它,finally 不保证执行

yield vs sleep 精确对比

复制代码
yield  : 无参数,立即执行;Running → Runnable(就绪);调度器仍可立刻再选中它
sleep  : 需时间参数;Running → Timed Waiting(阻塞);调度器不会给阻塞态分配时间片

打断标记的三条铁律

  1. 打断运行中 的线程 → 标记置为 true,线程继续跑,由它自己决定退出("体面地打断")。
  2. 打断 sleep/wait/join 中的线程 → 抛 InterruptedException标记被清空为 false
  3. 因此 catch (InterruptedException e) 里若想继续退出,必须重新 interrupt() 恢复标记

线程状态:Java 六态 vs 操作系统五态

Java 的 Thread.State六态,与 OS 五态(新建/就绪/运行/阻塞/终止)不是一回事:

Java 状态 含义 进入方式
NEW 创建但未 start() new Thread()
RUNNABLE 运行 + 就绪 + OS 层面的 IO 阻塞,JVM 统统算 RUNNABLE start() / 被唤醒后抢到锁
BLOCKED 等待 synchronized 的 Monitor 锁(EntryList) 竞争 monitor 失败
WAITING 无时限等待 wait() / join() / LockSupport.park()
TIMED_WAITING 有时限等待 sleep(n) / wait(n) / join(n) / parkNanos
TERMINATED 已结束 run() 正常返回或异常终止

状态转换速记图

复制代码
        start()                      获取锁失败
  NEW ──────────> RUNNABLE ─────────────────────> BLOCKED ──(抢到锁)──> RUNNABLE
                    │  │                              ▲                    ▲
      wait()/join() │  │ sleep(n)/wait(n)/join(n)      │ notify 后需重新抢锁 │
      /park()       │  │ /parkNanos                    └────────────────────┘
                    ▼  ▼
                WAITING   TIMED_WAITING
                    │  │
      notify/notifyAll/unpark/超时
                    └──┴────────> RUNNABLE ──(run 结束)──> TERMINATED

关键区分BLOCKEDWAITING 都不占用 CPU,但唤醒路径不同 ------BLOCKED 由"锁释放"唤醒,WAITING 由 notify/unpark 唤醒,且唤醒后不直接获得锁,要先进入 EntryList 重新竞争(即先变 BLOCKED 再变 RUNNABLE)。


三 JMM 与三大特性:可见性、原子性、有序性

三大特性与保障手段

特性 问题根源 保障手段
原子性 操作被线程调度打断(如 i++ 是读-改-写三步) synchronized、锁、CAS 原子类
可见性 线程把共享变量拷到工作内存,改了别的线程不一定立刻看到 volatilesynchronized、锁、final
有序性 编译器/CPU 指令重排 volatile(内存屏障)、synchronized、happens-before

volatile:两层语义 + 一个不能

  • 可见性 :对 volatile 变量写指令后加写屏障sfence,HotSpot 用 lock 前缀指令实现),强制刷主存;读指令前加读屏障,强制从主存加载。
  • 有序性:写屏障禁止它之前的代码被重排到屏障后;读屏障禁止它之后的代码被重排到屏障前。
  • 不保证原子性volatile int i; i++ 依然是错的,因为无法阻止"指令交错"。

经典场景:DCL 单例为什么必须 volatile

java 复制代码
// 字节码:17:new → 20:dup → 21:invokespecial <init> → 24:putstatic INSTANCE
// 21(初始化) 和 24(赋值引用) 可能被重排 → 24 先执行时,别的线程拿到"半初始化"对象
private static volatile Singleton INSTANCE;   // JDK 5 起 volatile 语义增强后才真正有效

happens-before 规则

  1. 程序次序规则:一个线程内,书写在前的操作 happens-before 书写在后的操作
  2. volatile 规则:volatile 写 happens-before 后续对该变量的读
  3. 锁规则:解锁 happens-before 后续加锁
  4. start 规则thread.start() happens-before 该线程的任何动作
  5. join 规则 :线程的所有动作 happens-before join() 返回
  6. 传递性:A→B、B→C ⇒ A→C
  7. 线程中断规则:interrupt() happens-before 被中断线程检测到中断
  8. 对象终结规则:构造函数结束 happens-before finalize()

final 的可见性

final 字段赋值后 JVM 会插入写屏障 ,保证其它线程不会读到 0 / null 的默认值------这是"不可变对象天然线程安全"的底层依据之一。

什么是线程安全?

当多个线程访问某个类时,不管运行时环境采用何种调度方式、这些线程如何交替执行 ,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,则称这个类是线程安全的。

判断一个变量是否线程安全,看三件事:是否被多线程共享是否有写操作是否做了同步


四 管程与 synchronized

synchronized 的三种用法

用法 锁对象
synchronized 实例方法 this(当前实例)
synchronized 静态方法 类对象 Xxx.class
synchronized(obj) {} 代码块 指定的 obj

铁律多个线程的锁必须是同一个对象才有互斥效果 ,且所有线程都要上锁。

字节码层面 :同步代码块 = monitorenter / monitorexit 指令对;同步方法靠方法表里的 ACC_SYNCHRONIZED 标志(指令里看不到)。

Monitor(管程)结构

每个 Java 对象都可以关联一个 Monitor:

复制代码
        ┌───────────────────────── Monitor ─────────────────────────┐
        │                                                           │
  Owner │  持有锁的线程(同一时刻只能有一个)                          │
        │                                                           │
EntryList│  竞争锁失败的线程 → BLOCKED(等 Owner 释放时被唤醒)          │
        │                                                           │
 WaitSet │  调用 wait() 的线程 → WAITING(等 notify/notifyAll)         │
        └───────────────────────────────────────────────────────────┘

对象头 Mark Word 与锁状态(64 位 HotSpot)

状态 Mark Word 布局 锁标志位
Normal(无锁) `hashcode:31 age:4
Biased(偏向锁) `thread:54 epoch:2
Lightweight Locked `ptr_to_lock_record:62 00`
Heavyweight Locked `ptr_to_heavyweight_monitor:62 10`
Marked for GC (空) 11

可用 JOL(org.openjdk.jol:jol-core)打印对象头验证。

锁升级完整链路(JDK 6~14 )

复制代码
  无锁 ──① 首次加锁(CAS 写线程ID)──> 偏向锁 ──② 出现第二个线程竞争──> 轻量级锁
                                                    │
                                            ③ 自旋失败 / 竞争激烈
                                                    ▼
                                              重量级锁(Monitor,OS mutex)
阶段 机制 关键点
偏向锁 首次用 CAS 把 Thread ID 写进 Mark Word,之后只需比对线程 ID,零 CAS 偏向锁解锁后线程 ID 仍留在对象头 ;Mark Word 没有存 hashCode 的位置,所以调用 hashCode() 会强制撤销偏向锁
轻量级锁 线程栈帧建 Lock Record,CAS 把 Mark Word 换成指向 Lock Record 的指针 重入时再压一条 Lock Record(值为 null)作计数;CAS 失败 ⇒ 锁膨胀
锁膨胀 申请 Monitor,Mark Word 指向 Monitor 指针,竞争线程进 EntryList 阻塞 解锁时 CAS 恢复失败 ⇒ 走重量级解锁流程
自旋优化 重量级锁竞争时先自旋,避免立即阻塞 Java 6 起自适应自旋 (按上次成功情况动态调整);Java 7 起无法关闭自旋;单核自旋纯属浪费

偏向锁的四种撤销场景

  1. 调用对象的 hashCode() → 撤销偏向(轻量锁记在 Lock Record、重量锁记在 Monitor)
  2. 其它线程使用该对象 → 升级为轻量级锁
  3. 调用 wait() / notify()直接膨胀为重量级锁(wait/notify 必须依托 Monitor)
  4. 批量重偏向 / 批量撤销:同一类对象撤销偏向 超过 20 次 → JVM 对该类"批量重偏向";超过 40 次 → 该类所有对象(含新建)不可偏向

相关 JVM 参数-XX:+UseBiasedLocking-XX:BiasedLockingStartupDelay=0(取消默认 4 秒延迟)、-XX:+EliminateLocks(锁消除,默认开启)、-XX:+UseSpinning

【版本边界 · 必考】JDK 15(JEP 374)已默认禁用并废弃偏向锁,JDK 18 起相关参数被标记为 obsolete。

  • JDK 6~14:无锁 → 偏向 → 轻量 → 重量(四态
  • JDK 15+(含 17/21/25):无锁 → 轻量级锁 → 重量级锁(三态)
  • 原因:现代应用多为高并发,偏向锁收益下降、撤销需 STW、维护成本高。如果你在 2026 年面试还把"偏向锁"当默认路径讲,会直接暴露答案是 2019 年的。

锁消除与锁粗化(JIT 优化)

  • 锁消除 :JIT 通过逃逸分析 发现锁对象不会逃出当前线程(如方法内局部 new Object()),直接去掉锁。实测:加锁与不加锁均约 1.5 ns/op ;用 -XX:-EliminateLocks 关闭后为 16.9 ns/op
  • 锁粗化 :对同一对象连续多次加锁/解锁(如循环里 synchronized)合并成一次,减少开销。

线程八锁(经典例题,本质是"锁的到底是谁")

答题模板:

  1. 先看锁对象:this 还是 Xxx.class------两者互不影响
  2. synchronized 方法与无锁方法不互斥
  3. 同一对象的多个 synchronized 实例方法互斥
  4. 静态方法上的 synchronized 锁的是类对象,与实例锁不是一把锁

情况3:3 1s 12 或 23 1s 1 或 32 1s 1

java 复制代码
@Slf4j(topic = "c.Number")
class Number{
 public synchronized void a() {
 sleep(1);
 log.debug("1");
 }
 public synchronized void b() {
 log.debug("2");
 }
 public void c() {
 log.debug("3");
 }
}
public static void main(String[] args) {
 Number n1 = new Number();
 new Thread(()->{ n1.a(); }).start();
 new Thread(()->{ n1.b(); }).start();
 new Thread(()->{ n1.c(); }).start();
}

情况5:2 1s 后 1

java 复制代码
@Slf4j(topic = "c.Number")
class Number{
 public static synchronized void a() {
 sleep(1);
 log.debug("1");
 }
 public synchronized void b() {
 log.debug("2");
 }
}
public static void main(String[] args) {
 Number n1 = new Number();
 new Thread(()->{ n1.a(); }).start();
 new Thread(()->{ n1.b(); }).start();
}

情况8:1s 后12, 或 2 1s后 1

java 复制代码
@Slf4j(topic = "c.Number")
class Number{
 public static synchronized void a() {
 sleep(1);
 log.debug("1");
 }
 public static synchronized void b() {
 log.debug("2");
 }
}
public static void main(String[] args) {
 Number n1 = new Number();
 Number n2 = new Number();
 new Thread(()->{ n1.a(); }).start();
 new Thread(()->{ n2.b(); }).start();
}

wait / notify / notifyAll

要点 说明
调用前提 必须在 synchronized ,否则抛 IllegalMonitorStateException
wait() 释放锁 并进入 WaitSet(WAITING);被唤醒后仍需重新抢锁
wait(n) 带超时版本
notify() 随机唤醒 WaitSet 中一个
notifyAll() 唤醒 WaitSet 中全部生产推荐

正确姿势(防虚假唤醒)

java 复制代码
synchronized (lock) {
    while (条件不满足) {     // ❗必须用 while,不能用 if
        lock.wait();
    }
    // 干活
    lock.notifyAll();
}

sleep vs wait(送分题)

维度 sleep wait
所属 Thread 静态 Object 实例
释放锁
使用前提 任意位置 必须在同步块内
唤醒 时间到 notify/notifyAll
状态 TIMED_WAITING WAITING / TIMED_WAITING

park / unpark(LockSupport)

每个线程持有一个 Parker 对象,内部有许可计数器 _counter(0 表示无许可):

复制代码
park()   : _counter == 0 → 阻塞;_counter == 1 → 消费掉,直接返回
unpark(t): _counter 置 1,若线程正阻塞则唤醒它

相比 wait/notify 的三大优势

  1. 以线程为单位,不需要锁对象、不需要同步块
  2. 可以先 unpark 再 park(许可不会丢),wait/notify 顺序反了会永久等待
  3. 更底层,AQS 全部基于它

注意 :连续多次 unpark 只保留一份许可,不会累加。

join 的原理(保护性暂停)

java 复制代码
// t1.join() 语义等价于:
synchronized (t1) {
    while (t1.isAlive()) { t1.wait(0); }
}

活跃性:死锁、活锁、饥饿

问题 定义 解法
死锁 多线程互相持有对方需要的锁,永久阻塞 按固定全局顺序加锁 (如按账户 id 排序);用 tryLock(timeout)
活锁 线程互相"谦让"不断重试,看似在跑实则无进展 引入随机退避时间
饥饿 某线程长期抢不到 CPU/锁 公平锁 / 合理线程数

死锁四必要条件 (缺一不可):互斥、占有且等待、不可抢占循环等待。破坏任一即可预防。

排查jstack <pid>,JVM 会直接打印 Found one Java-level deadlock 及互相等待的锁与栈。

哲学家就餐(经典场景题):5 人 5 筷,全拿左筷 → 死锁。两种解法:

  1. 信号量限流new Semaphore(4),最多 4 人同时就餐 → 破坏循环等待
  2. 奇偶异序:奇数号先拿左、偶数号先拿右 → 破坏循环等待

参考资料

黑马程序员深入学习Java并发编程,JUC并发编程全套教程

Java Guide Java并发常见面试题总结

相关推荐
比兔代理1 小时前
路由器静态IP怎么设置?光猫、WAN口与局域网配置教程
网络·智能路由器
我命由我123451 小时前
Android 控件 - ListAdapter
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
adaierya1 小时前
用 AI 解决音频转换编程问题
开发语言·人工智能·python·分类·ai编程
何何____1 小时前
web方向js考核题解
开发语言·前端·javascript
CodeStats1 小时前
【Java 类加载器】Java 类加载器完整体系深度拆解(中):URLClassLoader 能力剖析与继承委派辨析
java·jvm·classloader·类加载器
心易行者1 小时前
用HTML在线运行搭后台管理系统:5个核心模块+0服务器,3天跑通完整业务
大数据·前端·网络·人工智能·python
Terra.K1 小时前
JAVA职业探索和学习----中间件开发目标
java·学习·中间件
ThornArmor1 小时前
重铸1996|首秀点亮:编写 helloworld 并用 Makefile 自动化收割第一个 .z64 卡带镜像
开发语言
船厂电气自动化ai大模型1 小时前
AI大模型与数学 第73课 秩的核心定义、矩阵秩的通用求解方法
开发语言·数据结构·机器学习