Java 并发编程筑基篇知识文档

适用读者:Java 基础复习 / 校招面试 / 工程实践入门
维护说明:本文档按"基础概念 → 底层原理 → 工程工具 → 实战练习"组织,可直接作为复习大纲。


目录


第一部分:基础概念

1.1 进程、线程、协程

这三个是"代码执行单元"的三个粒度,从大到小。

进程(Process)

  • 定义:操作系统资源分配的基本单位。一个运行的程序就是一个进程。
  • 特征:拥有独立的内存空间(虚拟地址空间)、文件描述符、堆栈等。进程间内存隔离,通信(IPC)成本高。
  • 例子:开一个 IDEA 是一个进程,开一个 Chrome 又是一个进程。

线程(Thread)

  • 定义:CPU 调度的基本单位,是进程内的执行流。一个进程至少有一个线程(主线程)。
  • 特征:同一进程内的线程共享堆、方法区,但各自有独立的栈、程序计数器(PC)、寄存器
  • 关键区别:线程是"谁占用 CPU"的最小单位,进程是"拥有资源"的单位。

协程(Coroutine / Fiber)

  • 定义:用户态的轻量级执行单元,由程序(而非操作系统)调度。也叫"微线程"“绿色线程”。
  • 特征:
    • 切换不进入内核态,成本极低(几十纳秒级 vs 线程微秒级)。
    • 在单线程内协作式调度(主动让出 yield),不需要锁。
    • 一个线程可跑成千上万个协程。
  • 谁在用:Go 的 goroutine、Python 的 async/await、Kotlin 协程、Java 21 的虚拟线程(Virtual Thread / Project Loom)。

三者对比

维度 进程 线程 协程
调度者 操作系统 操作系统 用户程序
切换成本 高(内核态) 中(内核态) 低(用户态)
内存隔离 独立 共享堆 共享(同线程内)
通信方式 IPC(管道/信号/共享内存) 共享内存+锁 直接访问(无锁)
数量上限 几十~几百 几百~几千 几万~几十万

💡 工程视角:Java 长期只有"线程",协程要靠第三方(Quasar)或反应式框架(Reactor/WebFlux)模拟;Java 21 的虚拟线程补上了这块短板,让写同步阻塞代码也能享受协程的并发能力。


1.2 并发、并行、串行、同步、异步

这是两组概念,容易混在一起,要拆开理解。

执行方式维度:并发 vs 并行 vs 串行

  • 串行(Serial):任务一个接一个执行,A 完成才做 B。单核单线程天然串行。
  • 并发(Concurrency):多个任务在"同一时间段"内推进,宏观上同时,微观上可能交替(时间片轮转)。强调"处理多件事的能力"。
  • 并行(Parallelism):多个任务在"同一时刻"真正同时执行,必须有多核/多 CPU。强调"同时做多件事"。

🎯 经典比喻(Rob Pike):

  • 并发 = 一个咖啡机,两个人轮流用(交替处理)。
  • 并行 = 两个咖啡机,两个人同时用。

口诀:并发是"同时应对",并行是"同时执行"。并发是结构设计,并行是硬件执行。

  • 关系:并发是目标(应对多任务),并行是手段(真正同时跑)。单核也能并发(靠时间片),但单核无法并行(多线程任务)。

调用方式维度:同步 vs 异步

关注的是调用者要不要等结果

  • 同步(Synchronous):调用后主动等待结果返回,期间阻塞当前执行流。
  • 异步(Asynchronous):调用后立即返回,结果通过回调、Future、事件等在将来通知。

注意:同步/异步 与 阻塞/非阻塞 是两组概念,常被混用。

  • 同步/异步 → 关注调用者是否主动等结果
  • 阻塞/非阻塞 → 关注被调用者在等待期间是否挂起调用线程
  • 组合如:同步阻塞(BIO)、同步非阻塞(NIO)、异步非阻塞(AIO)。

一句话串联

多线程是为了并发(应对多任务),在多核上自然就并行了;当某个任务要等 I/O,我们希望它异步,别把线程卡死。


1.3 线程原理

1. 线程的生命周期(OS 层)

  • 新建 → 就绪 → 运行 → 阻塞 → 终止
  • 就绪态的线程等待 CPU 时间片;拿到时间片就运行;时间片用完或被抢占就回就绪;等 I/O、锁、sleep 时进入阻塞。

2. Java 线程的六种状态(Thread.State)

状态 含义
NEW 创建了 Thread 对象,还没 start()
RUNNABLE 在 JVM 看来"可运行"(包含 OS 的就绪+运行,Java 不区分)
BLOCKED 等待 synchronized 锁(监视器锁)
WAITING 无限等待(wait() / join() / LockSupport.park() 无超时)
TIMED_WAITING 有时限的等待(sleep(t) / wait(t) / join(t))
TERMINATED 线程执行完毕

⚠️ 易错点:BLOCKED 只针对 synchronized 锁;用 ReentrantLock 等待时线程处于 WAITING,不是 BLOCKED

3. 上下文切换(Context Switch)

线程从 A 切到 B 时,OS 要保存 A 的现场(PC、寄存器、栈指针)并恢复 B 的现场,这个动作叫上下文切换。

  • 切换有成本(微秒级),且会破坏 CPU 缓存亲和性。
  • 线程不是越多越好:线程过多 → 切换开销增大 → 反而变慢。这是线程池要控制大小的根本原因。

4. 线程的实现模型

  • 内核级线程(1:1):一个用户线程对应一个内核线程。Java HotSpot 默认就是这种(Linux 上是 pthread)。调度由 OS 负责,功能强但切换贵。
  • 用户级线程(N:1):多个用户线程映射到一个内核线程,库调度。轻但无法利用多核、阻塞会卡死全部。
  • 混合模型(M:N):多个用户线程映射到多个内核线程。Go、Java 21 虚拟线程走这条路。

5. 线程安全与可见性原理

  • 可见性:一个线程改了共享变量,另一个线程看不到 → 因为 CPU 缓存 + 指令重排序。解决:volatilesynchronizedfinal
  • 原子性:操作不可分割。i++ 不是原子的。解决:synchronizedAtomicXxx
  • 有序性:编译器/CPU 可能重排指令。volatile 插入内存屏障禁止重排。

1.4 Java 线程的创建方式

方式 1:继承 Thread

class MyThread extends Thread {
    public void run() { System.out.println("running"); }
}
new MyThread().start();
  • 缺点:Java 单继承,继承了 Thread 就不能再继承别的类;耦合了"任务"和"执行机制"。

方式 2:实现 Runnable 接口(推荐)

Runnable task = () -> System.out.println("running");
new Thread(task).start();
  • 优点:任务与执行分离,无单继承限制,适合共享资源。
  • Thread 本身也实现了 Runnable

方式 3:实现 Callable + FutureTask(有返回值)

Callable<Integer> task = () -> 42;
FutureTask<Integer> future = new FutureTask<>(task);
new Thread(future).start();
Integer result = future.get(); // 阻塞获取结果
  • Runnable 的区别:Callablecall() 有返回值且能抛受检异常
  • Future.get() 会阻塞直到任务完成。

方式 4:线程池 ExecutorService(工程推荐)

ExecutorService pool = Executors.newFixedThreadPool(4);
Future<Integer> f = pool.submit(() -> 42);
pool.shutdown();
  • 优点:复用线程、控制并发数、统一管理。
  • ⚠️ 阿里规约:不要用 Executors 的快捷方法(队列无界可能 OOM),要用 ThreadPoolExecutor 手动构造。

方式 5(Java 21+):虚拟线程

Thread.startVirtualThread(() -> doIo());
  • 轻量、海量、适合 I/O 密集场景。

🎯 取舍口诀:无返回值用 Runnable,有返回值用 Callable,生产环境一律走线程池,别裸 new Thread()


1.5 Java 线程重点方法

run() vs start()(最高频考点)

start() run()
作用 启动新线程,由 JVM 调度其 run() 普通方法调用
是否新建线程 ✅ 是 ❌ 否,在当前线程同步执行
能否多次调用 ❌ 一个线程只能 start() 一次(再调抛 IllegalThreadStateException) ✅ 可多次调用(普通方法)

🚨 易错:new Thread(task).run(); 不会启动新线程,只是在主线程里把 run 跑一遍 —— 这是常见 bug。

sleep(long millis)

  • 静态方法(Thread.sleep),让当前线程休眠,进入 TIMED_WAITING
  • 不释放锁(这是和 wait() 的核心区别)。
  • 休眠期间被 interrupt() 会抛 InterruptedException

wait() / notify() / notifyAll()(属于 Object,不是 Thread)

  • 必须在 synchronized 块内调用(持有对象监视器),否则抛 IllegalMonitorStateException
  • wait()释放锁并进入 WAITING(对象等待集);被唤醒后要重新竞争锁。
  • 用于生产者-消费者模型。

sleep vs wait 对比(必背):

sleep wait
归属 Thread 静态方法 Object 实例方法
释放锁 不释放 释放
唤醒 超时或 interrupt notify/超时/interrupt
前置条件 必须持有该对象锁

join() —— 等待线程结束

t.join(); // 当前线程阻塞,直到 t 执行完
  • 内部用 wait() 实现,进入 WAITING/TIMED_WAITING
  • 典型场景:主线程等所有子线程算完再汇总。

yield() —— 礼让

  • 提示调度器:当前线程愿意让出 CPU。只是建议,不保证会被切换。
  • 进入就绪态(不是阻塞态),可能立刻又被调度。

interrupt() —— 中断协作(重要)

  • Java 用协作式中断:不强制停止线程,而是设置中断标志,由线程自己决定如何响应。
  • 检查:Thread.currentThread().isInterrupted()(不清标志)、Thread.interrupted()(静态,会清标志)。
  • 若线程正阻塞在 sleep/wait/join,会抛 InterruptedException 并清除标志
  • 正确写法:
while (!Thread.currentThread().isInterrupted()) {
    // ... 干活
}
// 捕获 InterruptedException 后应:重新设置中断标志 或 退出

setDaemon(boolean) —— 守护线程

  • 必须在 start() 前设置。
  • 守护线程(如 GC 线程)的特性:所有非守护线程结束,JVM 就退出,守护线程会被强制终止(不保证 finally 执行完)。
  • 别在守护线程里做关键 I/O。

其他常用方法

方法 说明
getName()/setName() 线程名
getPriority()/setPriority() 优先级(1~10,默认 5,仅建议性)
getId() 线程唯一 ID
Thread.currentThread() 获取当前线程引用
getState() 获取线程状态(六种之一)
Thread.holdsLock(obj) 判断当前线程是否持有某对象锁

第二部分:进阶原理

2.1 synchronized 与 Lock(AQS)

1. synchronized 的底层原理

**Monitor(监视器)**机制:每个 Java 对象天生带一个 Monitor(对应 C++ 的 ObjectMonitor)。

  • 字节码层面:
    • 同步代码块 → monitorenter / monitorexit 指令(正常退出 + 异常退出各一个 monitorexit)。
    • 同步方法 → 方法表中的 ACC_SYNCHRONIZED 标志位。
  • ObjectMonitor 关键字段:_owner(持有线程)、_EntryList(等锁队列 → BLOCKED)、_WaitSet(wait 队列 → WAITING)。
  • 加锁本质:线程进 _EntryList 排队 → 抢到锁后 _owner 指向自己。

2. 锁升级(JDK 6 的重大优化,高频考点)

早期 synchronized 一律走 OS 互斥量(重量级),JDK 6 引入锁升级,在对象头的 Mark Word 里记录锁状态:

无锁 → 偏向锁 → 轻量级锁 → 重量级锁   (只能升,不能降)
触发条件 原理 适用场景
偏向锁 第一个线程访问 Mark Word 记录线程 ID,之后该线程进入无需 CAS 单线程反复进入同步块
轻量级锁 多线程交替(无真实竞争) 用 CAS + 栈帧中的 Lock Record 持锁时间短、无竞争
重量级锁 真正竞争 膨胀为 OS mutex,未抢到的线程阻塞挂起 高竞争、持锁时间长

💡 中间还有自旋锁:竞争时先"原地转圈"忙等一会儿,赌持锁者很快释放,避免昂贵的内核态挂起。JDK 6+ 自适应自旋(根据历史成功率动态调整)。

⚠️ JDK 15 起偏向锁被废弃(-XX:+UseBiasedLocking 失效)——收益越来越小,维护成本高。面试知道即可。

3. AQS(AbstractQueuedSynchronizer)—— Lock 的基石

ReentrantLockSemaphoreCountDownLatchReentrantReadWriteLock 都基于 AQS。

  • 核心两件套:
    • volatile int state:同步状态(0 无锁,>0 持有/重入次数)。
    • 一个 CLH 变体 FIFO 双向队列:管理等待线程。
  • 工作流:tryAcquire 用 CAS 改 state → 成功拿到锁 → 失败则封装成 Node 入队 → LockSupport.park() 挂起 → 前驱唤醒后 unpark()
  • 设计模式:模板方法。AQS 定义骨架(入队、挂起、唤醒),子类只实现 tryAcquire/tryRelease(独占)或 tryAcquireShared(共享)。
  • 公平 vs 非公平:非公平锁新线程直接插队抢(吞吐高),公平锁严格按队列顺序。

4. synchronized vs Lock 对比(必背)

维度 synchronized Lock(如 ReentrantLock)
本质 JVM 关键字 JUC 接口/类
释放锁 自动(出作用域/异常) 必须手动 finally { lock.unlock() }
响应中断 lockInterruptibly()
超时/尝试获取 tryLock(time)
公平性 仅非公平 可选公平/非公平
条件变量 1 个(wait/notify) 多个 Condition
可重入

口诀:简单同步用 synchronized(自动释放、够用),需要"中断/超时/多条件/公平"时才上 Lock


2.2 volatile

1. 两大语义

  1. 保证可见性:volatile 变量的写立即对其他线程可见。底层借助 CPU 缓存一致性协议(MESI) + 写屏障把 Store Buffer 刷入主内存,读屏障强制重新加载。
  2. 禁止指令重排序:插入内存屏障(Memory Barrier)

2. 不保证原子性 ❗

volatile int i = 0;
i++;   // 读-改-写 三步,仍然不安全!

要原子性 → 用 AtomicIntegersynchronized

3. 四种内存屏障

LoadLoadLoadStoreStoreStoreStoreLoad(最贵,全能屏障)。JMM 在 volatile 写前插 StoreStore、写后插 StoreLoad;volatile 读后插 LoadLoad + LoadStore。

4. happens-before

volatile 写 happens-before 后续对同一变量的 volatile 读 —— 这是可见性的形式化保证。

5. 经典应用:DCL 单例

public class Singleton {
    private static volatile Singleton instance;   // 必须 volatile!
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

为什么必须 volatile? new Singleton() 在字节码上是三步:

  1. 分配内存
  2. 调用构造(初始化)
  3. 赋值引用给 instance

若 2、3 重排序,另一个线程可能在 instance != null 时拿到未初始化完的对象 → NPE。volatile 禁止重排,消除这个窗口。


2.3 线程池

1. 七大参数

new ThreadPoolExecutor(
    int corePoolSize,                // 核心线程数
    int maximumPoolSize,             // 最大线程数
    long keepAliveTime,              // 非核心线程空闲存活时间
    TimeUnit unit,                   // 时间单位
    BlockingQueue<Runnable> workQueue,           // 任务队列
    ThreadFactory threadFactory,                 // 线程工厂(命名很有用)
    RejectedExecutionHandler handler             // 拒绝策略
);

2. 工作流程(顺序是核心!)

提交任务
  │
  ▼
核心线程未满? ──是──▶ 创建【核心线程】执行
  │否
  ▼
任务队列未满? ──是──▶ 放入【队列】等待
  │否
  ▼
未达最大线程数? ──是──▶ 创建【非核心线程】执行
  │否
  ▼
执行【拒绝策略】

🎯 口诀:核心 → 队列 → 最大 → 拒绝
⚠️ 易错:是先排队再扩容到 max,不是先扩容。这和很多人直觉相反,务必记住。

3. 四种拒绝策略

策略 行为
AbortPolicy(默认) RejectedExecutionException
CallerRunsPolicy 提交任务的线程自己跑(天然反压,生产常用)
DiscardPolicy 静默丢弃新任务
DiscardOldestPolicy 丢掉队列最老的任务,再重试提交

4. ⚠️ 为什么禁用 Executors 快捷方法(阿里规约)

方法 风险
newFixedThreadPool 队列是 LinkedBlockingQueue(无界)→ OOM
newSingleThreadExecutor 同上,无界队列
newCachedThreadPool max = Integer.MAX_VALUE → 创建过多线程 OOM

→ 一律用 ThreadPoolExecutor 手动构造,显式指定有界队列 + 合理的线程数。

5. 线程数怎么定

  • CPU 密集型:N + 1(N = CPU 核心数)。
  • IO 密集型:2N,或公式 N × (1 + 等待时间/计算时间)

6. execute vs submit

execute(Runnable) submit(Callable/Runnable)
返回值 Future
异常 直接抛出 封装在 Future,需 future.get() 才暴露

2.4 ThreadLocal

1. 作用

给每个线程一份独立副本,实现线程隔离。场景:数据库连接、SimpleDateFormat(非线程安全)、用户 Session/上下文。

2. 原理(关键结构)

Thread ──持有──▶ ThreadLocalMap (threadLocals 字段)
                     │
                     └─ Entry[]
                          key   = ThreadLocal 对象 (弱引用 WeakReference)
                          value = 实际值 (强引用)

threadLocal.set(v) 实际是写到当前线程ThreadLocalMap 里,跟其他线程完全隔离。

3. 为什么 key 用弱引用?

防止 ThreadLocal 对象本身泄漏:业务代码里的强引用消失后,key 可被 GC 回收。但注意:这只能防 key 泄漏,防不了 value。

4. 内存泄漏的真正原因

key 被回收 → 变成 null,但 value 仍是强引用。如果线程长期存活(尤其是线程池复用线程),这些 key=null 的 entry 的 value 永久驻留 → 泄漏。

5. 解决办法

ThreadLocal<User> ctx = new ThreadLocal<>();
try {
    ctx.set(user);
    // 业务逻辑
} finally {
    ctx.remove();   // ★ 线程池场景必做!
}

ThreadLocal 在 get/set 时会顺带清理 key=null 的 entry,但别依赖它,线程池里务必手动 remove()

6. 父子线程传递

  • 普通 ThreadLocal:子线程拿不到父线程的值。
  • InheritableThreadLocal:创建子线程时拷贝一份。
  • ⚠️ 线程池场景线程被复用,InheritableThreadLocal 会失效/错乱 → 用阿里开源的 TransmittableThreadLocal(TTL)

第三部分:实战练习

题 1:两个线程交替打印 1~100(三种解法对比)

解法 A:wait/notify(经典)

class Printer {
    private int num = 1;
    private boolean flag = true; // true:线程1打印,false:线程2打印
    public synchronized void print1() throws InterruptedException {
        while (num <= 100) {
            while (!flag) wait();
            if (num > 100) break;
            System.out.println(Thread.currentThread().getName() + ": " + num++);
            flag = false;
            notifyAll();
        }
    }
    public synchronized void print2() throws InterruptedException {
        while (num <= 100) {
            while (flag) wait();
            if (num > 100) break;
            System.out.println(Thread.currentThread().getName() + ": " + num++);
            flag = true;
            notifyAll();
        }
    }
}

解法 B:Lock + Condition(更精确,推荐)

Lock lock = new ReentrantLock();
Condition c1 = lock.newCondition();
Condition c2 = lock.newCondition();
// 线程1打印完 c2.signal(); 自己 c1.await();
// 线程2打印完 c1.signal(); 自己 c2.await();

解法 C:volatile + 自旋(无锁,简单但费 CPU)

volatile int turn = 1; // 轮到谁
// 线程1: while(turn!=1) ; 打印; turn=2;

三种解法正好串起 synchronizedLock/Conditionvolatile 的用法,面试爱问。

题 2:生产者-消费者(阻塞队列版)

BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(10);
// 生产者:queue.put(x);   满了自动阻塞
// 消费者:queue.take();    空了自动阻塞

一行 put/take 搞定,这就是阻塞队列的价值。

题 3:三个线程顺序打印 A B C(循环 10 次)

思路同题 1 的 Lock + 3 个 Condition,A 唤醒 B、B 唤醒 C、C 唤醒 A,形成环。


附录:知识地图与术语表

知识地图

          可见性/有序性          互斥(原子)
volatile ─────────────▶  synchronized / Lock(AQS)
   │                          │
   │单例DCL                    │锁升级:偏向→轻量→重量
   │                          │
线程隔离                      线程协作
ThreadLocal ─────▶ wait/notify, Condition, BlockingQueue
   │remove防泄漏
管理线程 ──▶ 线程池(核心→队列→最大→拒绝)

一图串起基础概念

进程 (资源分配单位)
 └─ 线程 (CPU 调度单位, 1:1 内核线程)
     ├─ run/start       创建与启动
     ├─ sleep/wait/join 阻塞与协作
     ├─ interrupt       协作式中断
     └─ 协程/虚拟线程   (用户态, 轻量, Java21+)

并发(应对多任务)  ──多核──▶  并行(同时执行)
                ▲
        线程/协程/异步 I/O 都是为了并发

术语速查

术语 一句话解释
并发 同一时间段内处理多件事(宏观同时)
并行 同一时刻真正同时执行(需多核)
临界区 访问共享资源的代码段
竞态条件 多线程并发访问共享变量导致结果错误
死锁 线程互相等待对方释放锁,永久阻塞
活锁 线程不断改变状态却无法推进
饥饿 线程长期得不到 CPU/锁
happens-before 前一操作的结果对后一操作可见的保证
CAS Compare And Swap,无锁原子操作
AQS 抽象队列同步器,JUC 锁的基石
JMM Java 内存模型,规定线程间如何交互
MESI CPU 缓存一致性协议

文档结束。后续可继续补充:JUC 并发工具(CountDownLatch/CyclicBarrier/Semaphore/CompletableFuture)、并发设计模式

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐