【JUC】基础:并发模型、锁机制与 JMM

目录

一、并发和并行

三种执行方式

对比总结

面试常问

二、锁机制

[1. 偏向锁(Biased Locking)](#1. 偏向锁(Biased Locking))

[2. 轻量级锁(Lightweight Locking)](#2. 轻量级锁(Lightweight Locking))

[3. 重量级锁(Heavyweight Locking)](#3. 重量级锁(Heavyweight Locking))

[4. 自旋锁(Spin Lock)](#4. 自旋锁(Spin Lock))

锁机制对比

[5. 锁消除(Lock Elimination)](#5. 锁消除(Lock Elimination))

[6. 锁粗化(Lock Coarsening)](#6. 锁粗化(Lock Coarsening))

[三、JMM 内存模型](#三、JMM 内存模型)

[为什么要 JMM](#为什么要 JMM)

抽象结构:主内存与工作内存

三大特性

[happens-before 原则](#happens-before 原则)

一个经典的可见性问题

四、总结


一、并发和并行

三种执行方式

1. 顺序执行(串行)

程序一条指令接一条指令地执行,前一个任务没做完,后一个就不能开始。

复制代码
// 顺序执行:先做完 taskA,再做 taskB
public static void main(String[] args) {
    taskA(); // 耗时 3s
    taskB(); // 耗时 3s
    // 总耗时:6s
}

2. 并发执行(Concurrent)

同一时间段内,多个任务交替执行。宏观上"同时进行",微观上是 CPU 在多个任务之间快速切换(时间片轮转)。单核 CPU 上的"多任务"本质都是并发。

复制代码
// 并发执行:两个任务交替使用 CPU
Thread t1 = new Thread(() -> taskA());
Thread t2 = new Thread(() -> taskB());
t1.start();
t2.start();
// 单核 CPU 上:总耗时接近 6s,但期间可以响应用户输入(不被 taskA 阻塞)

3. 并行执行(Parallel)

同一时刻,多个任务真正地同时执行。并行依赖多核 CPU------每个核心跑一个任务,互不干扰。

复制代码
单核 CPU(并发):              四核 CPU(并行):
CPU: A A B B A A B B            Core1: A A A A A A
     ↑ 时间片轮转切换             Core2: B B B B B B
                                 (A、B 真正同时运行)

对比总结

维度 顺序执行 并发 并行
任务执行方式 一个做完再做下一个 交替执行(切换) 真正同时执行
依赖硬件 单核即可 单核即可 必须多核
关注点 简单、可预测 任务调度、吞吐量 计算能力、速度
典型场景 脚本、批处理 Web 服务器响应请求 大数据计算、并行流

一句话区分:

并发 是"轮流处理多件事"的能力(结构设计),并行是"同时处理多件事"的能力(硬件能力)。并发是逻辑层的概念,并行是物理层的概念。

面试常问

  • Q:单核 CPU 能实现并行吗?
    A:不能。单核任一时刻只能执行一条指令,只能通过时间片切换实现并发。
  • Q:多线程一定是并行的吗?
    A:不一定。线程数多于核心数时,依然是并发切换为主;只有在多核上多个线程被同时调度到不同核心时,才是并行。

二、锁机制

Java 中的 synchronized 并不是一开始就使用重量级互斥锁,而是有一套锁升级路径:

复制代码
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
(锁只能升级,不能降级,JDK 15 之前如此)

1. 偏向锁(Biased Locking)

适用场景:始终只有一个线程访问同步块。很多场景下锁根本不存在竞争(比如一个 Vector 只被一个线程用),加锁解锁的开销纯属浪费。

工作方式:

  1. 第一次线程访问同步块时,在对象头的 Mark Word 中记录当前线程 ID;
  2. 之后该线程再进入,只需比对 Mark Word 中的线程 ID 是否是自己,无需任何 CAS 或互斥操作
  3. 一旦有第二个线程来竞争,偏向锁升级为轻量级锁。

本质:用"记住是谁"代替"反复确认"。 把同步开销降到接近零。

2. 轻量级锁(Lightweight Locking)

适用场景:多个线程交替执行同步块,但同一时刻没有实际竞争(竞争不激烈、持有时间短)。

工作方式:

  1. 线程进入同步块前,在当前线程栈帧中开辟一块 Lock Record 空间;
  2. 将对象 Mark Word 拷贝到 Lock Record 中;
  3. 通过 CAS 尝试把对象 Mark Word 替换为指向 Lock Record 的指针,成功则获得锁;
  4. 失败说明有竞争,线程先自旋等待;
  5. 自旋一定次数(自适应自旋)后仍失败,膨胀为重量级锁。

3. 重量级锁(Heavyweight Locking)

适用场景:实际存在多个线程同时竞争

  • 依赖操作系统的 Mutex(互斥量) 实现;
  • 涉及用户态 ↔ 内核态切换,开销大(线程的挂起与唤醒都要操作系统介入);
  • 优点是不自旋消耗 CPU,竞争激烈时更稳定。

4. 自旋锁(Spin Lock)

轻量级锁膨胀前的过渡手段:拿不到锁时,线程不挂起,而是空转几个循环,赌"锁马上就释放了"。

  • 优点:避免了内核态切换的开销,如果锁很快释放,整体性能更高;
  • 缺点:自旋期间占用 CPU;如果锁被长时间持有,纯浪费 CPU;
  • JDK 6 引入自适应自旋:根据上一次在同一个锁上的自旋时间和持有者状态,动态调整自旋次数。

锁机制对比

维度 偏向锁 轻量级锁 重量级锁
适用场景 只有一个线程访问 多线程交替、无实际竞争 多线程实际竞争
实现方式 Mark Word 记录线程 ID CAS + 栈帧 Lock Record OS Mutex
加锁开销 几乎为零 CAS 操作(用户态) 内核态切换
阻塞挂起 否(自旋)
状态标识 Mark Word 偏向位=1 Mark Word 指向栈帧 Mark Word 指向 monitor

5. 锁消除(Lock Elimination)

JIT 编译器在运行时,通过逃逸分析 发现某个锁对象根本不可能被其他线程访问,于是直接把锁删掉

复制代码
public String concat(String s1, String s2) {
    // StringBuffer 的方法都是 synchronized 的
    // 但 sb 是局部变量,不会逃逸出这个方法
    // JIT 会消除掉所有内部的加锁操作,性能等同 StringBuilder
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    return sb.toString();
}

6. 锁粗化(Lock Coarsening)

通常我们建议缩小同步块,但反过来:如果一系列连续操作都对同一个对象反复加锁解锁 (比如循环里调 StringBuffer.append),JIT 会把锁的范围扩大(粗化)到整个操作序列之外,只加一次锁。

复制代码
for (int i = 0; i < 100; i++) {
    synchronized (lock) {   // 优化前:加锁 100 次
        doSomething(i);
    }
}

// JIT 粗化后等价于:
synchronized (lock) {       // 只加锁 1 次
    for (int i = 0; i < 100; i++) {
        doSomething(i);
    }
}

三、JMM 内存模型

为什么要 JMM

硬件层面,CPU 有多级缓存(L1/L2/L3),每个核心有自己的缓存副本。多线程下会出现两个致命问题:

  1. 可见性:线程 A 修改了变量,线程 B 可能看到的还是旧值(写在 A 的工作内存里,没刷到主存);
  2. 有序性 :编译器和 CPU 为了优化会进行指令重排序,多线程下执行顺序可能与代码顺序不一致。

JMM(Java Memory Model,Java 内存模型) 是 JVM 定义的一套抽象规范,用来屏蔽不同硬件和操作系统的内存访问差异,让 Java 程序在所有平台上都能获得一致的内存可见性保证。

抽象结构:主内存与工作内存

复制代码
        Java 内存模型(JMM)
┌─────────────────────────────────────┐
│           主内存(Main Memory)        │
│        所有共享变量存放在这里           │
└─────────────────────────────────────┘
        ↑ load / save            ↑ load / save
┌───────────────┐        ┌───────────────┐
│ 线程 A 工作内存 │        │ 线程 B 工作内存 │
│ 变量的私有副本  │        │ 变量的私有副本  │
└───────────────┘        └───────────────┘
  • 主内存:所有线程共享,存放共享变量(实例字段、静态字段);
  • 工作内存:每个线程私有,保存了该线程使用到的变量的主内存副本;
  • 线程对变量的所有操作(读、写)都必须在工作内存 中进行,不能直接读写主内存
  • 线程之间无法直接访问对方的工作内存,传递必须经过主内存。

注意:JMM 的主内存/工作内存与 JVM 的堆/栈是不同层次的划分,大致上主内存对应堆,工作内存对应栈+寄存器/缓存,但不要画等号。

三大特性

特性 含义 解决手段
原子性 操作不可分割,要么全部执行,要么不执行 synchronizedLock、原子类(CAS)
可见性 一个线程的修改,其他线程立即可见 volatilesynchronizedfinal
有序性 禁止指令重排序带来的乱序 volatilesynchronized(锁保证串行语义)

happens-before 原则

JMM 不可能禁止所有重排序(否则性能崩溃),于是定义了一套天然有序的规则------只要两个操作之间存在 happens-before 关系,前一个操作的结果就对后一个操作可见,且不会重排序:

  1. 程序顺序规则:单线程内,前面的操作 happens-before 后面的操作;
  2. 锁规则:解锁 happens-before 后续对同一把锁的加锁;
  3. volatile 规则:对 volatile 变量的写 happens-before 后续的读;
  4. 线程启动规则Thread.start() happens-before 该线程内的所有操作;
  5. 线程终止规则 :线程内的所有操作 happens-before 其他线程检测到它终止(join() 返回);
  6. 传递性:A hb B,B hb C ⇒ A hb C。

核心思想:为程序员提供强易用性(看起来顺序执行),为编译器/处理器提供最大优化自由(在规则内随便重排)。

一个经典的可见性问题

复制代码
public class VisibilityDemo {
    private static boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (flag) {
                // 可能永远停不下来:
                // 读的是本线程工作内存中的旧值
            }
            System.out.println("线程结束");
        }).start();

        Thread.sleep(1000);
        flag = false; // 主线程修改,子线程可能看不到
    }
}

加上 volatile 即可修复:

复制代码
private static volatile boolean flag = true;
// volatile 保证:写立即刷回主内存 + 使其他线程的缓存副本失效

四、总结

知识点 一句话记忆
并发 vs 并行 并发是"轮流"(逻辑),并行是"同时"(硬件)
锁升级 无锁 → 偏向锁 → 轻量级锁 → 重量级锁,逐级膨胀
偏向锁 只有一个线程访问,Mark Word 记线程 ID,加锁近零开销
轻量级锁 CAS + 自旋,避免内核态切换
重量级锁 OS Mutex,内核态切换,竞争激烈时使用
锁消除/粗化 JIT 基于逃逸分析删掉无用锁 / 合并连续锁
JMM 主内存 + 工作内存的抽象模型,解决可见性与有序性
happens-before 判断多线程操作是否可见、是否有序的唯一依据
相关推荐
曹牧1 小时前
SpringBoot: @RequestBody String
java
爱划水不秃头的程序员1 小时前
堆 栈 和常量池的关系是啥 是jvm对他们进行的划分吗
java
周GZ1 小时前
4个提问,deepseek讲解Java中的线程池
java
LuTshoes1 小时前
context 上下文工程
java·人工智能·spring·ai
wuminyu1 小时前
Markword在紧凑对象头上的实现原理剖析
java·linux·c语言·jvm·c++
君顾12 小时前
24小时自助健身房系统开发实战与完整指南
java·开发语言·健身房
鹿角片ljp2 小时前
LeetCode 78:子集|回溯、选与不选、递归和path快照
java·数据结构·算法
金玉满堂@bj2 小时前
多环境部署方案(开发、测试、生产,搭配Tomcat\+WAR包)
java·tomcat·maven
白远山2 小时前
无人自助健身平台搭建:从架构设计到设备联动的完整实战
java·开发语言·架构·需求分析