覆盖 Java 多线程从基础概念、核心原理到企业级实战的全量知识点,整合所有对话内容的核心考点、易错点、代码示例,适合日常开发巩固、面试复习使用。


第一章 多线程基础核心概念

1.1 进程与线程的核心区别

核心定义

  • 进程:操作系统资源分配的最小单位,每个进程有独立的内存地址空间,进程间相互隔离。
  • 线程:CPU调度执行的最小单位,也叫轻量级进程,一个进程内可包含多个线程,线程共享进程的资源,仅持有少量私有数据(栈、程序计数器)。

表格

对比维度进程线程
资源开销独立内存空间,创建 / 销毁 / 切换开销极大共享进程资源,仅持有少量私有数据,开销极小
包含关系操作系统运行基本单位,可包含 1~N 个线程进程的执行单元,无法脱离进程独立存在
通信方式复杂,需管道、Socket、共享内存、信号量等 IPC 机制简单,直接共享进程堆、方法区资源,通过共享变量即可通信
健壮性进程间隔离,一个崩溃不影响其他进程共享资源,一个线程崩溃会导致整个进程崩溃
调度开销切换需更换整个地址空间,Cache 失效,开销极高仅切换上下文,开销极低

面试易错点:线程不是越小越好,过多线程会导致 CPU 频繁上下文切换,反而降低性能。


1.2 并行与并发的本质区别

表格

特性并发(Concurrent)并行(Parallel)
核心定义宏观上同一时间段处理多个任务,微观上交替执行微观上同一时刻多个任务同时执行
硬件依赖单核 CPU 即可实现必须依赖多核 CPU
通俗类比一个人轮流咬 3 个包子3 个人同时各吃 1 个包子

面试易错点:高并发设计不等于并行执行,并发是设计能力,并行是硬件执行能力。


第二章 Java 线程的创建与核心方法

2.1 线程创建的 4 种方式

Java 官方定义 2 种基础创建方式,工程中扩展 2 种常用方式,核心区别在于「线程载体」与「业务逻辑」的解耦程度。

方式 1:继承 Thread 类,重写 run () 方法

核心原理:Thread 类是 JDK 提供的具体公共类,实现了 Runnable 接口,可直接实例化。继承后重写 run (),定义线程的业务逻辑。

java

运行

// 自定义线程类
public class MyThread extends Thread {
    // 重写run():线程的业务执行入口
    @Override
    public void run() {
        System.out.println("线程执行:" + Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        t1.start(); // 启动线程,JVM自动回调run()
    }
}

优缺点

  • 优点:写法简单,可直接操作线程对象
  • 缺点:Java 单继承限制,无法再继承其他类;线程与业务逻辑耦合,无法复用
方式 2:实现 Runnable 接口,重写 run () 方法

核心原理:将业务逻辑封装在 Runnable 实现类中,传入 Thread 对象,实现「线程载体」与「业务任务」的解耦。

java

运行

// 自定义任务类
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("线程执行:" + Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        MyRunnable mr = new MyRunnable();
        // Thread提供了接收Runnable的构造方法,绑定任务
        Thread t1 = new Thread(mr);
        Thread t2 = new Thread(mr); // 同一个任务可被多个线程执行
        t1.start();
        t2.start();
    }
}

优缺点

  • 优点:解耦彻底,避免单继承限制,任务可复用,是基础方式中的首选
  • 缺点:无返回值,无法抛出受检异常
方式 3:实现 Callable 接口,配合 FutureTask 使用

核心原理:JDK1.5 引入,支持泛型返回值、抛出受检异常,通过 FutureTask 包装后传入 Thread,可获取线程执行结果。

java

运行

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        System.out.println("线程执行计算");
        return 1 + 1; // 支持返回值
    }

    public static void main(String[] args) throws Exception {
        FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable());
        new Thread(futureTask).start();
        // 阻塞获取线程执行结果
        Integer result = futureTask.get();
        System.out.println("执行结果:" + result);
    }
}
方式 4:通过线程池创建线程(企业级唯一推荐)

核心原理:通过 ThreadPoolExecutor 手动创建 / Executors 工具类创建线程池,复用线程,避免频繁创建销毁的开销,统一管理线程生命周期。

java

运行

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 创建单线程池(示例,生产推荐手动创建ThreadPoolExecutor)
        ExecutorService executor = Executors.newSingleThreadExecutor();
        executor.submit(() -> System.out.println("线程池执行任务"));
        executor.shutdown(); // 关闭线程池
    }
}

2.2 run () 和 start () 的核心区别

这是面试超高频考点,核心是「线程启动」和「业务执行」的职责分离。

表格

对比维度start()run()
核心作用启动线程的唯一合法方式,向操作系统申请创建内核线程线程的业务逻辑入口,只是一个普通成员方法
线程状态调用后线程从 NEW→RUNNABLE,等待 CPU 调度直接调用不会启动新线程,只会在当前调用线程中同步执行
调用次数一个线程对象只能调用一次,多次调用抛出 IllegalThreadStateException可无限次重复调用,和普通方法无区别
执行特性异步执行,调用后立即返回,run () 在新线程中异步执行同步执行,必须等 run () 执行完毕才会继续往下执行

面试易错点:直接调用 run () 不会启动新线程,只有调用 start () 才会真正创建线程。


2.3 Runnable 和 Callable 的核心区别

表格

对比维度RunnableCallable
核心方法void run()V call() throws Exception
返回值无返回值支持泛型返回值,可获取线程执行结果
异常处理不能抛出受检异常,只能内部捕获可直接抛出受检异常,异常可被 Future 对象捕获
JDK 版本JDK1.0 引入JDK1.5 引入
适用场景无返回结果的异步任务有返回结果的异步任务

第三章 Java 线程的生命周期(6 种状态)

Java 线程的状态完全由java.lang.Thread.State枚举定义,共 6 种,状态流转严格遵循 JVM 规范。

3.1 6 种状态的核心定义

  1. NEW(新建):线程对象已创建,但未调用 start (),未进入调度队列。
  2. RUNNABLE(可运行):Java 统一封装了操作系统的「就绪 Ready」和「运行 Running」状态。
    • 就绪:已调用 start (),等待 CPU 分配时间片
    • 运行:已获得 CPU 时间片,正在执行 run () 方法
  3. BLOCKED(阻塞):线程等待获取 synchronized 排他锁,未拿到锁时进入该状态,拿到锁后回到 RUNNABLE。
  4. WAITING(无限等待):线程主动进入无限等待,需被其他线程显式唤醒,不会自动超时。触发场景:Object.wait () 无超时、Thread.join () 无超时、LockSupport.park ()
  5. TIMED_WAITING(计时等待):线程进入限时等待,超时后自动唤醒,也可被提前唤醒。触发场景:Thread.sleep (long)、Object.wait (long)、Thread.join (long)、LockSupport.parkNanos ()
  6. TERMINATED(终止):线程 run () 执行完毕,或异常退出,生命周期结束,无法再次启动。

3.2 核心状态流转

plaintext

NEW → start() → RUNNABLE
RUNNABLE → 未获取synchronized锁 → BLOCKED → 拿到锁 → RUNNABLE
RUNNABLE → wait/join/park → WAITING → notify/notifyAll/unpark → RUNNABLE
RUNNABLE → sleep/wait(long) → TIMED_WAITING → 超时/唤醒 → RUNNABLE
RUNNABLE → 执行完成/异常退出 → TERMINATED

面试易错点:Thread.sleep () 只会让线程进入 TIMED_WAITING,不会释放锁,也不会进入 BLOCKED 状态。


第四章 多线程核心方法对比与原理

4.1 wait () vs sleep () 全维度对比

面试超高频核心考点,重点关注「锁特性」的差异。

表格

对比维度wait()sleep()
所属类java.lang.Object 的实例方法java.lang.Thread 的静态方法
锁的处理必须在 synchronized 同步上下文中调用,调用后会释放持有的对象锁可在任意位置调用,调用后不会释放任何锁
使用前提必须在 synchronized 代码块 / 方法中使用,否则抛出 IllegalMonitorStateException无同步上下文要求
核心用途线程间通信、协调,实现等待 - 唤醒机制仅让当前线程暂停指定时间,让出 CPU 资源
唤醒方式需 notify ()/notifyAll () 唤醒,带超时版本可超时自动唤醒到达休眠时间自动唤醒,仅可被 interrupt () 中断唤醒
中断响应抛出 InterruptedException,响应中断,清除中断标志同左

关键易错点

  1. sleep()没有无参版本,必须传入时间参数,无参写法编译直接报错。
  2. sleep()无法被 notify ()/notifyAll () 唤醒,notify 仅能唤醒同一锁对象上调用 wait () 的线程。

4.2 线程中断机制(打断唤醒)

核心本质

thread.interrupt()不会暴力杀死线程,仅会将线程内部的「中断标志位」设置为 true,后续处理由线程自身决定。

两种响应场景
  1. 线程处于 sleep ()/wait ()/join () 阻塞状态:立即响应中断,抛出 InterruptedException,自动清除中断标志位(重置为 false),这就是「打断唤醒」。

    java

    运行

    Thread sleepThread = new Thread(() -> {
        try {
            Thread.sleep(10000); // 休眠10秒
        } catch (InterruptedException e) {
            // 被interrupt()打断唤醒,进入异常处理
            System.out.println("线程被中断唤醒");
            Thread.currentThread().interrupt(); // 重新设置中断标志位
        }
    });
    sleepThread.start();
    Thread.sleep(1000);
    sleepThread.interrupt(); // 发送中断信号,打断休眠
    
  2. 线程处于正常运行状态:不会主动抛出异常,需手动通过Thread.currentThread().isInterrupted()检查标志位,决定是否停止。

4.3 notify () vs notifyAll () 核心区别

两个方法均为 Object 类的本地方法,必须在 synchronized 同步上下文中调用,用于唤醒 wait () 的线程。

  1. notify():随机唤醒一个处于该对象等待队列中的线程,被唤醒的线程进入锁竞争队列。
    • 风险:可能出现「信号丢失」,唤醒错误的线程,导致所有线程无法执行,引发死锁。
  2. notifyAll():唤醒所有处于该对象等待队列中的线程,所有线程进入锁竞争队列,竞争到锁的线程执行,其余回到等待状态。
    • 优势:更安全,无信号丢失风险,是绝大多数场景的首选;缺点是有轻微的锁竞争性能开销。

4.4 保证多线程顺序执行的 3 种方案

方案 1:Thread.join ()(最简单原生方案)

join () 的作用是让当前线程等待调用 join () 的线程执行完毕后,再继续执行。

java

运行

public class ThreadOrderDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> System.out.println("线程1执行"));
        Thread t2 = new Thread(() -> System.out.println("线程2执行"));
        Thread t3 = new Thread(() -> System.out.println("线程3执行"));

        t1.start();
        t1.join(); // 主线程等待t1执行完,再启动t2
        t2.start();
        t2.join(); // 主线程等待t2执行完,再启动t3
        t3.start();
    }
}
方案 2:单线程池 SingleThreadExecutor

单线程池只有一个工作线程,提交的任务会按提交顺序串行执行,天然保证顺序。

java

运行

ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> System.out.println("线程1执行"));
executor.submit(() -> System.out.println("线程2执行"));
executor.submit(() -> System.out.println("线程3执行"));
executor.shutdown();
方案 3:CompletableFuture 串行编排(JDK8 + 企业级推荐)

基于异步回调实现顺序执行,支持复杂的任务编排。

java

运行

CompletableFuture.runAsync(() -> System.out.println("线程1执行"))
        .thenRun(() -> System.out.println("线程2执行"))
        .thenRun(() -> System.out.println("线程3执行"))
        .join();

第五章 线程的优雅停止方案

5.1 废弃方法的坑

绝对禁止使用 stop ()、suspend ()、resume ():这些方法已被 JDK 废弃,会暴力终止线程,强制释放所有锁,导致数据不一致、资源泄漏等严重问题。

5.2 正确停止方案 1:volatile 标记位(非阻塞循环场景)

核心原理:volatile 修饰的布尔标记位保证多线程可见性,线程在循环中持续判断标记位,外部修改后线程正常退出。

java

运行

public class StopThreadDemo {
    // volatile保证多线程可见性
    private static volatile boolean isStop = false;

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (!isStop) {
                System.out.println("线程正在运行");
            }
            System.out.println("线程正常停止");
        });
        thread.start();

        Thread.sleep(1000);
        isStop = true; // 修改标记位,通知线程停止
    }
}

适用场景:线程执行非阻塞的循环任务,无 sleep/wait 等阻塞操作。

5.3 正确停止方案 2:interrupt 中断机制(通用方案,适配阻塞场景)

核心原理:通过 interrupt () 发送中断信号,同时适配正常运行和阻塞场景,是最通用的优雅停止方案。

java

运行

public class InterruptStopDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            // 非阻塞场景:判断中断标志
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    // 阻塞场景:中断会抛出InterruptedException
                    Thread.sleep(500);
                    System.out.println("线程正在运行");
                } catch (InterruptedException e) {
                    // 捕获中断异常,收尾后退出
                    System.out.println("线程被中断,准备退出");
                    Thread.currentThread().interrupt(); // 重置中断标志
                    break;
                }
            }
        });
        thread.start();

        Thread.sleep(2000);
        thread.interrupt(); // 发起中断通知
    }
}

5.4 正确停止方案 3:Future.cancel ()(线程池 / Callable 场景)

核心原理:线程池提交的任务,通过 Future 的 cancel (true) 方法发起中断,停止任务执行。

java

运行

ExecutorService executor = Executors.newSingleThreadExecutor();
Future<?> future = executor.submit(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        System.out.println("任务正在运行");
    }
});

Thread.sleep(1000);
future.cancel(true); // 中断任务,停止线程执行
executor.shutdown();

第六章 synchronized 锁核心原理与实战

6.1 synchronized 的底层本质

Java 中每个对象都自带一把内置锁(监视器锁 Monitor),锁的物理载体存储在对象头的 Mark Word 中。synchronized 的核心是保证同一时刻,只有一个线程能拿到对象的锁,执行同步代码块。

语法语义synchronized (lock) 表示「当前线程要获取 lock 引用指向的对象的内置锁,获取成功则执行代码块,失败则阻塞等待」。

  • lock只是一个对象引用变量,不是锁本身,真正的锁是它指向的堆内存中的对象。
  • 变量名不影响锁的效果,只要指向同一个对象,就是同一把锁。

6.2 锁对象的使用规范

✅ 推荐写法

使用private static final Object作为锁对象,是最稳妥的标准写法:

java

运行

public class SafeDemo {
    // 私有:避免外部误用,保证封装性
    // static:类级别锁,全局唯一
    // final:保证引用不可变,避免锁失效
    private static final Object LOCK = new Object();

    public void doSomething() {
        synchronized (LOCK) {
            // 业务逻辑
        }
    }
}
❌ 禁止 / 不推荐写法
  1. this当锁:外部可拿到实例对象误用锁,破坏封装性,可能导致死锁。
  2. 用 String 常量、Integer 缓存对象当锁:JVM 中全局共享 / 缓存复用,极易导致锁失效、死锁。
  3. 锁对象引用可变:比如lock = new Object()重新赋值,导致不同线程用不同的锁,完全失效。

6.3 细粒度锁实现:lockMap 的核心用法

核心作用

lockMap本质是ConcurrentHashMap<业务ID, 锁对象>,是「业务 ID→锁对象」的映射表,实现细粒度锁,避免全局锁的性能瓶颈。

解决的痛点

全局锁会让所有业务请求串行执行,哪怕业务之间完全无关;而 lockMap 为每个业务(商品、账户、请求)创建独立的锁,只有同一业务的请求才会竞争锁,不同业务完全并行,极大提升并发性能。

标准代码实现

java

运行

import java.util.concurrent.ConcurrentHashMap;

public class StockService {
    // 商品ID → 剩余库存
    private final ConcurrentHashMap<Long, Integer> stockDB = new ConcurrentHashMap<>();
    // lockMap:商品ID → 对应商品的锁对象
    private final ConcurrentHashMap<Long, Object> lockMap = new ConcurrentHashMap<>();

    // 初始化商品库存
    public void initStock(Long productId, Integer initStock) {
        stockDB.put(productId, initStock);
        lockMap.putIfAbsent(productId, new Object());
    }

    // 扣减库存核心方法
    public boolean deductStock(Long productId, Integer buyNum) {
        // 1. 获取当前商品的专属锁对象
        Object productLock = lockMap.get(productId);
        if (productLock == null) {
            throw new RuntimeException("商品不存在");
        }

        // 2. 只锁当前商品,不影响其他商品
        synchronized (productLock) {
            // 3. 加锁后再次校验库存
            Integer currentStock = stockDB.get(productId);
            if (currentStock == null || currentStock < buyNum) {
                System.out.println("库存不足");
                return false;
            }

            // 4. 原子扣减库存
            stockDB.put(productId, currentStock - buyNum);
            System.out.printf("商品%d扣减成功,剩余库存%d%n", productId, currentStock - buyNum);
            return true;
        }
    }
}
核心方法说明
  • lockMap.putIfAbsent(key, value):原子操作,仅当 key 不存在时才存入,保证同一个业务 ID 永远对应同一个锁对象,避免多线程下锁覆盖。
  • lockMap.get(key):根据业务 ID 获取对应的锁对象,用于 synchronized 加锁。

6.4 企业级 5 大实战场景

  1. 电商库存扣减:按商品维度加细粒度锁,避免超卖,保证并发安全。
  2. 账户转账:固定加锁顺序(按账户 ID 从小到大),避免死锁,保证资金一致性。
  3. 接口防重复提交:按幂等号加锁,配合双重校验,避免重复提交。
  4. 双重检查锁(DCL)单例模式:配合 volatile 禁止指令重排,实现线程安全的懒汉式单例。
  5. 本地缓存并发读写:加锁保证缓存读写的原子性,避免脏读、并发修改异常。

第七章 volatile 关键字核心原理

7.1 Java 内存模型(JMM)与可见性问题

JMM 将内存分为两类:

  1. 主内存:所有线程共享,存储共享变量的 “真身”。
  2. 工作内存:每个线程私有,线程读写变量必须先把主内存的变量复制到工作内存,操作后再择机刷回主内存。

可见性问题:一个线程修改了主内存的共享变量,其他线程看不到,依然使用自己工作内存的旧副本,导致逻辑错误。

7.2 volatile 的两大核心作用

1. 保证共享变量修改的立即可见性

volatile 对 JVM 强制约束:

  • 线程修改 volatile 变量后,必须立即强制刷回主内存
  • 线程每次读取 volatile 变量时,必须强制从主内存重新读取最新值,不能使用工作内存的旧副本。

这也是flag标记位必须用 volatile 修饰的核心原因:保证主线程修改 flag 后,子线程能立即看到,及时停止线程,避免死循环。

2. 禁止指令重排序

JVM 为了优化性能,会在不改变单线程语义的前提下,打乱代码执行顺序(指令重排序)。volatile 会通过「内存屏障」禁止 JVM 对该变量相关的代码进行重排序,保证代码按书写顺序执行。

经典应用场景:双重检查锁(DCL)单例模式,禁止对象初始化的指令重排,避免多线程拿到半初始化的对象。

7.3 关键易错点

volatile只能保证可见性和禁止指令重排,无法保证原子性。比如volatile int count; count++,count++ 是「读 - 改 - 写」三步操作,多线程下依然会出现并发安全问题,需要加锁保证原子性。


第八章 JVM 栈内存与多线程

8.1 栈内存与栈帧的核心定义

  • 栈内存(虚拟机栈):每个线程私有的内存区域,线程创建时自动分配,线程销毁时自动释放,不存在线程安全问题。
  • 栈帧:每个方法执行时,都会创建一个栈帧,存储方法的局部变量表、操作数栈、方法出口等信息。方法调用入栈,方法执行完毕出栈。

8.2 栈内存大小与线程数的关系

JVM 的栈内存大小通过-Xss参数设置,默认通常为 1024k(1M)。

  • 每个线程启动时,JVM 都会为它分配一块固定大小的栈内存。
  • 机器 / 进程的可用内存是固定的,单个线程的栈内存越大,能创建的总线程数就越少

示例:

  • 进程可用内存 512M,栈内存默认 1M,理论最大线程数≈512 个。
  • 若把栈内存改为 2M,理论最大线程数直接减半≈256 个。

8.3 栈内存不是越大越好的核心原因

  1. 限制总线程数:栈内存越大,能创建的线程数越少,高并发场景下会导致无法创建新线程,抛出OutOfMemoryError: unable to create new native thread异常。
  2. 内存浪费:绝大多数业务方法不需要太大的栈内存,默认 1M 完全足够,设置过大只会浪费内存资源。
  3. 栈溢出风险降低有限StackOverflowError通常是方法递归调用过深导致,单纯增大栈内存只能缓解,无法根治,反而会带来上述问题。

最佳实践:无特殊需求(如深度递归、大量局部变量),保持 JVM 默认栈内存大小即可;高并发场景可适当调小栈内存,提升可创建的线程数上限。


面试高频考点速记

  1. 进程是资源分配最小单位,线程是 CPU 调度最小单位。
  2. 启动线程只能用 start (),直接调用 run () 不会创建新线程,使用主线程执行。
  3. wait () 会释放锁,sleep () 不会释放锁;sleep 只能被中断或超时唤醒,不能被 notify 唤醒。
  4. 停止线程优先用 interrupt 中断机制,或 volatile 标记位,禁止用 stop ()。
  5. volatile 保证可见性、禁止指令重排,不保证原子性。
  6. synchronized 锁的是对象,不是代码;优先用细粒度锁,避免全局锁。
  7. 栈内存不是越大越好,栈内存越大,能创建的线程数越少。
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐