首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >深入浅出Java并发多线程:核心基础+内存模型+死锁——从用法到原理

深入浅出Java并发多线程:核心基础+内存模型+死锁——从用法到原理

原创
作者头像
IT大佬 jzit-top
发布2026-07-26 16:10:39
发布2026-07-26 16:10:39
900
举报

深入浅出Java并发多线程:核心基础+内存模型+死锁——从用法到原理

并发编程是Java进阶的"分水岭",掌握它,你就拿到了大厂面试的入场券。


写在前面

"线程安全"、"可见性"、"死锁"——这些词汇在Java面试中出现频率极高,但很多朋友对它们的理解停留在"背答案"层面:

  • synchronizedReentrantLock 有什么区别? → "一个是关键字,一个是类"
  • volatile 能保证原子性吗? → "不能"
  • 什么是死锁? → "两个线程互相等待"

但如果面试官追问一句:"synchronized 在 JVM 层面是怎么实现的?""volatile 的内存屏障到底是如何禁止指令重排序的?""写一个死锁,怎么用 jstack 定位?" ——很多人的回答就开始含糊了。

本文会从用法 → 原理 → 实战排查三个维度,把 Java 并发多线程的核心知识点彻底讲透,每一部分都配有代码示例和 JVM 层面的原理解析。


一、线程基础:从创建到生命周期

1.1 创建线程的三种方式

方式

实现

特点

继承 Thread

extends Thread,重写 run()

单一继承局限,不推荐

实现 Runnable

implements Runnable,实现 run()

更灵活,任务与线程分离

实现 Callable

implements Callable<T>,有返回值,可抛异常

配合 FutureTask 获取结果

代码示例

代码语言:javascript
复制
// 方式一:继承 Thread(不推荐)
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread running");
    }
}

// 方式二:实现 Runnable(推荐)
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Runnable running");
    }
}

// 方式三:实现 Callable + FutureTask(获取返回值)
class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        return "Callable result";
    }
}

// 使用示例
public static void main(String[] args) throws Exception {
    // Runnable 配合线程池使用更佳
    new Thread(new MyRunnable()).start();
    
    // Callable 获取返回值
    FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
    new Thread(futureTask).start();
    String result = futureTask.get(); // 阻塞等待结果
    System.out.println(result);
}

1.2 线程生命周期(六种状态)

关键理解

  • RUNNABLE 包含了"就绪"和"运行"两个状态,由操作系统调度
  • BLOCKEDsynchronized 专属,Lock 接口的等待是 WAITING
  • wait() 必须配合 synchronized 使用,会释放锁;sleep() 不释放锁

二、Java 内存模型(JMM)——并发问题的根源

2.1 为什么需要 JMM?

现代 CPU 为了性能,引入了多级缓存指令重排序。JMM 定义了 Java 程序在并发环境下的可见性、有序性、原子性规范。

JMM 抽象结构

代码语言:javascript
复制
┌─────────────┐          ┌─────────────┐
│   线程 A     │──────────│   线程 B     │
└──────┬──────┘          └──────┬──────┘
       │ 写入                   │ 读取
       ▼                        ▼
┌─────────────┐          ┌─────────────┐
│  工作内存    │          │  工作内存    │
│ (CPU 缓存)   │          │ (CPU 缓存)   │
└──────┬──────┘          └──────┬──────┘
       │                        │
       └──────────┬─────────────┘
                  ▼
          ┌─────────────┐
          │   主内存     │
          │  (堆内存)    │
          └─────────────┘

核心规则:线程对共享变量的所有操作都必须在工作内存中进行,不能直接读写主内存;不同线程之间不能直接访问对方的工作内存,变量值的传递必须通过主内存完成。

2.2 三大特性的实现原理

特性

含义

实现手段

原子性

操作不可中断

synchronized、Lock、Atomic 类

可见性

一个线程修改,其他线程立即感知

volatile、synchronized、final

有序性

代码按顺序执行(不重排序)

volatile 禁止重排序、happens-before 规则

2.3 volatile 的底层原理:内存屏障

volatile 修饰的变量有两个语义:

  1. 保证可见性:写操作会立即刷新到主内存,读操作会从主内存重新加载
  2. 禁止指令重排序:通过插入内存屏障(Memory Barrier)实现

JVM 对 volatile 的字节码处理

代码语言:javascript
复制
// Java 代码
volatile boolean flag = true;

// JVM 字节码层面
// 在 volatile 写操作前后插入内存屏障
// StoreStoreBarrier → volatile 写 → StoreLoadBarrier
// LoadLoadBarrier → volatile 读 → LoadStoreBarrier

HotSpot 源码中的实现orderAccess.hpp):

代码语言:javascript
复制
// x86 架构下,volatile 写会加上 lock 前缀指令
// lock addl $0x0,(%rsp)  —— 这是一个全屏障
inline void OrderAccess::fence() {
    if (os::is_MP()) {
        __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
    }
}

面试重点volatile 不保证原子性count++ 这种操作分为"读-改-写"三步,volatile 只能保证读和写各自是原子的,但三步合起来不是原子的。需要用 AtomicIntegersynchronized

2.4 happens-before 规则(判断数据竞争的关键)

如果两个操作满足 happens-before 关系,那么前一个操作的结果对后一个操作可见:

  1. 程序次序规则:同一线程中,前面的操作 happens-before 后面的
  2. volatile 规则:对 volatile 变量的写 happens-before 对该变量的读
  3. 锁规则:解锁 happens-before 后续的加锁
  4. 传递性:A happens-before B,B happens-before C → A happens-before C

三、synchronized 深度剖析——从用法到 JVM 实现

3.1 三种用法

代码语言:javascript
复制
// 1. 实例方法锁 —— 锁的是当前实例对象 this
public synchronized void method1() { ... }

// 2. 静态方法锁 —— 锁的是 Class 对象
public static synchronized void method2() { ... }

// 3. 代码块锁 —— 最灵活,可指定锁对象
public void method3() {
    synchronized (this) { ... }
    synchronized (lockObject) { ... }
}

3.2 JVM 层面实现:monitorenter / monitorexit

代码语言:javascript
复制
// 一段简单的同步代码块
public void add() {
    synchronized (this) {
        count++;
    }
}

// 对应的字节码
public void add();
    Code:
       0: aload_0          // 将 this 推入栈顶
       1: dup
       2: astore_1         // 存储到局部变量
       3: monitorenter     // ★ 进入同步块,获取锁
       4: aload_0
       5: dup
       6: getfield #2      // 获取 count 字段
       9: iconst_1
      10: iadd
      11: putfield #2      // 写回 count
      14: aload_1
      15: monitorexit      // ★ 退出同步块,释放锁
      16: goto 24
      19: astore_2
      20: aload_1
      21: monitorexit      // ★ 异常处理中也要释放锁
      22: aload_2
      23: athrow
      24: return

每个 Java 对象都关联一个 Monitor 对象monitorenter 尝试获取 Monitor 的所有权,获取成功后计数器 +1;monitorexit 计数器 -1,减到 0 时释放锁。

3.3 锁升级机制(JDK 1.6 优化)

JDK 1.6 引入了偏向锁 → 轻量级锁 → 重量级锁的升级过程,避免直接使用重量级的互斥量(Mutex)。

锁状态

适用场景

实现原理

无锁

无竞争

-

偏向锁

同一线程多次重入

Mark Word 存储线程 ID,CAS 替换

轻量级锁

少量线程竞争

CAS 自旋尝试获取,不阻塞

重量级锁

高并发竞争

操作系统的 Mutex,线程阻塞,上下文切换

锁升级流程

3.4 锁消除与锁粗化(JIT 编译器优化)

  • 锁消除:JIT 检测到某些锁不可能被多线程访问(如局部变量),直接去掉锁
  • 锁粗化:多个连续的锁操作合并为一个,减少获取/释放锁的开销

四、死锁——从代码复现到工具定位

4.1 死锁的四个必要条件

条件

说明

互斥

资源同一时间只能被一个线程占用

占有且等待

线程已占有一个资源,同时等待另一个资源

不可抢占

已占有的资源不能被强制剥夺

循环等待

线程之间形成资源等待环路

4.2 手写死锁 Demo

代码语言:javascript
复制
public class DeadlockDemo {
    private static final Object RESOURCE_A = new Object();
    private static final Object RESOURCE_B = new Object();

    public static void main(String[] args) {
        // 线程1:先拿 A,再拿 B
        Thread t1 = new Thread(() -> {
            synchronized (RESOURCE_A) {
                System.out.println("Thread1: 持有 A,等待 B...");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                synchronized (RESOURCE_B) {
                    System.out.println("Thread1: 同时持有 A 和 B");
                }
            }
        });

        // 线程2:先拿 B,再拿 A —— 形成死锁
        Thread t2 = new Thread(() -> {
            synchronized (RESOURCE_B) {
                System.out.println("Thread2: 持有 B,等待 A...");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                synchronized (RESOURCE_A) {
                    System.out.println("Thread2: 同时持有 A 和 B");
                }
            }
        });

        t1.start();
        t2.start();
    }
}

运行后程序卡住不动,没有输出"同时持有"的日志。

4.3 死锁定位实战(jstack + jps)

步骤一:用 jps 查看 Java 进程 PID

代码语言:javascript
复制
$ jps
12345 DeadlockDemo
12346 Jps

步骤二:用 jstack 导出线程快照

代码语言:javascript
复制
jstack 12345

输出关键信息

代码语言:javascript
复制
Found one Java-level deadlock:
=============================
"Thread1":
  waiting to lock monitor 0x00007f... (object 0x... <-- RESOURCE_B)
  which is held by "Thread2"
"Thread2":
  waiting to lock monitor 0x00007f... (object 0x... <-- RESOURCE_A)
  which is held by "Thread1"

Java stack information for the threads listed above:
===================================================
Thread1:
    at DeadlockDemo.lambda$main$0(DeadlockDemo.java:15)
    - waiting to lock <0x...> (a java.lang.Object)
    - locked <0x...> (a java.lang.Object)
Thread2:
    at DeadlockDemo.lambda$main$1(DeadlockDemo.java:26)
    - waiting to lock <0x...> (a java.lang.Object)
    - locked <0x...> (a java.lang.Object)

jstack 直接告诉你死锁的线程、锁对象和代码行号,定位非常高效。

4.4 死锁的解决方案

方案

说明

示例

破坏占有且等待

一次性申请所有资源

用 synchronized 嵌套时,统一锁顺序

破坏循环等待

所有线程按相同顺序加锁

都先拿 A 再拿 B

使用超时机制

尝试获取锁,超时放弃

ReentrantLock.tryLock(timeout, unit)

使用 Lock 接口

更灵活的锁控制

ReentrantLock + tryLock

推荐方案:统一加锁顺序 + ReentrantLock.tryLock 超时机制

代码语言:javascript
复制
// 使用 tryLock 避免死锁
public boolean transfer(ReentrantLock lockA, ReentrantLock lockB) {
    long timeout = 1000;
    while (true) {
        if (lockA.tryLock(timeout, TimeUnit.MILLISECONDS)) {
            try {
                if (lockB.tryLock(timeout, TimeUnit.MILLISECONDS)) {
                    try {
                        // 业务操作
                        return true;
                    } finally {
                        lockB.unlock();
                    }
                }
            } finally {
                lockA.unlock();
            }
        }
        // 超时重试
        if (--retryCount == 0) return false;
    }
}

五、并发工具类速查

工具类

作用

底层原理

AtomicInteger

原子操作 int

CAS(Unsafe.compareAndSwapInt)

ReentrantLock

可重入锁

AQS(AbstractQueuedSynchronizer)

CountDownLatch

等待多个线程完成

AQS 共享模式

CyclicBarrier

多线程互相等待到齐

ReentrantLock + Condition

Semaphore

控制并发线程数

AQS 共享模式

ConcurrentHashMap

线程安全 Map

CAS + synchronized(JDK 8)

BlockingQueue

阻塞队列

ReentrantLock + Condition

5.1 AQS 核心原理简析

AbstractQueuedSynchronizer 是 JUC 包的基石,核心是一个 volatile int state 变量 + CLH 双向队列

代码语言:javascript
复制
// AQS 核心结构
public abstract class AbstractQueuedSynchronizer {
    private volatile int state;        // 同步状态
    private transient volatile Node head;  // CLH 队列头
    private transient volatile Node tail;  // CLH 队列尾
    
    // 尝试获取锁(由子类实现)
    protected boolean tryAcquire(int arg) { ... }
    // 尝试释放锁(由子类实现)
    protected boolean tryRelease(int arg) { ... }
}
  • ReentrantLockstate 表示锁的重入次数
  • Semaphorestate 表示剩余许可证数量
  • CountDownLatchstate 表示剩余计数

六、面试高频追问与避坑指南

Q1:sleep() 和 wait() 的区别?

对比维度

sleep()

wait()

所属类

Thread 静态方法

Object 实例方法

是否释放锁

❌ 不释放

✅ 释放(必须在 synchronized 块中)

唤醒方式

时间到 / interrupt()

notify() / notifyAll() / 时间到

使用场景

线程暂停

线程间通信

Q2:为什么 wait() 必须放在 synchronized 块中?

:为了确保调用 wait() 前已经持有锁(Monitor)。否则会抛出 IllegalMonitorStateException。这是防止虚假唤醒保证 wait/notify 的可见性的基础。

Q3:volatile 能替代 synchronized 吗?

:不能。volatile 只保证可见性有序性,不保证原子性。对于 count++ 这种复合操作,必须用 synchronizedAtomicXXX

Q4:死锁和活锁的区别?

  • 死锁:线程互相等待对方释放资源,永远阻塞
  • 活锁:线程不断改变状态尝试执行,但始终无法推进(如两个人在窄路互相让路,但总是让到同一侧)

七、总结:并发知识脑图


最后的话:并发编程的学习没有捷径,但有一条高效的路径——写代码验证 → 读源码理解 → 用工具排查。建议你把文中的死锁 Demo 跑一遍,用 jstack 实际定位一次,这比背十道面试题都管用。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 深入浅出Java并发多线程:核心基础+内存模型+死锁——从用法到原理
    • 写在前面
    • 一、线程基础:从创建到生命周期
      • 1.1 创建线程的三种方式
      • 1.2 线程生命周期(六种状态)
    • 二、Java 内存模型(JMM)——并发问题的根源
      • 2.1 为什么需要 JMM?
      • 2.2 三大特性的实现原理
      • 2.3 volatile 的底层原理:内存屏障
      • 2.4 happens-before 规则(判断数据竞争的关键)
    • 三、synchronized 深度剖析——从用法到 JVM 实现
      • 3.1 三种用法
      • 3.2 JVM 层面实现:monitorenter / monitorexit
      • 3.3 锁升级机制(JDK 1.6 优化)
      • 3.4 锁消除与锁粗化(JIT 编译器优化)
    • 四、死锁——从代码复现到工具定位
      • 4.1 死锁的四个必要条件
      • 4.2 手写死锁 Demo
      • 4.3 死锁定位实战(jstack + jps)
      • 4.4 死锁的解决方案
    • 五、并发工具类速查
      • 5.1 AQS 核心原理简析
    • 六、面试高频追问与避坑指南
      • Q1:sleep() 和 wait() 的区别?
      • Q2:为什么 wait() 必须放在 synchronized 块中?
      • Q3:volatile 能替代 synchronized 吗?
      • Q4:死锁和活锁的区别?
    • 七、总结:并发知识脑图
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档