Java 线程与并发的系统化认知:四大骨架 + 状态过山车 + JMM 黄金三角 + 锁的三层金字塔

线程是操作系统能够进行运算调度的最小单位,多线程是为了把多核 CPU 的性能榨干到最后一滴。 

第一层:确立"四大核心骨架"(线程的出生路径)

让一段代码在新的线程中运行,工业界有且仅有四种标准写法。

1. 纯种流派:继承 Thread 类

class MyThread extends Thread {
    @Override
    public void run() { /* 业务代码 */ }
}
new MyThread().start();

致命缺点:Java 单继承,继承了 Thread 就无法再继承别的类,工业级业务设计中极其受限。

2. 现代主流:实现 Runnable 接口 ✅

class MyTask implements Runnable {
    @Override
    public void run() { /* 业务代码 */ }
}
new Thread(new MyTask()).start();

优点:解耦。你的类可以同时实现多个接口,彻底解决单继承痛点。

3. 带返回值的硬核派:实现 Callable 接口

run() 返回 void,如果线程执行完复杂计算后想要拿到结果,需要用 Callable

class CalcTask implements Callable<Integer> {
    @Override
    public Integer call() { return 42; }
}
FutureTask<Integer> ft = new FutureTask<>(new CalcTask());
new Thread(ft).start();
Integer result = ft.get(); // 阻塞等待计算结果

4. 工业终极杀招:线程池(ThreadPoolExecutor)⭐

生产铁律:严禁在业务代码里手动 new Thread()!线程的创建和销毁是非常昂贵的系统资源。高并发时每个请求都 new 一个线程,服务器会因频繁切换和内存耗尽而宕机。

ExecutorService executor = Executors.newFixedThreadPool(10);
executor.submit(() -> { /* 业务代码 */ });
executor.shutdown();

第二层:掌控"状态过山车"(线程的生命周期)

JVM 明确定义了 Java 线程的 6 种生存状态

                  ┌──────────┐
                  │   NEW    │ new 出 Thread 对象,还没调 start()
                  └────┬─────┘
                       │ start()
                       ▼
                  ┌──────────┐
        ┌─────────│RUNNABLE │←────────┐
        │         │ (可运行)  │         │
        │         └──┬──┬────┘         │
        │            │  │              │
        │   抢锁失败   │  │  sleep(1000) │
        │            │  │  wait(1000)  │
        ▼            ▼  │  parkNanos()  │
  ┌──────────┐   ┌──────┴──────┐      │
  │ BLOCKED  │   │TIMED_WAITING│      │
  │ (阻塞)    │   │  (限时等待)  │      │
  └──────────┘   └──────┬──────┘      │
                        │ 时间到/notify │
                        └──────┬───────┘
                               │
                         ┌─────▼──────┐
                         │  WAITING   │ wait()/park()
                         │ (无限等待)   │ 需 notify/unpark 唤醒
                         └─────┬──────┘
                               │
                         ┌─────▼──────┐
                         │ TERMINATED │
                         │  (终止)     │
                         └────────────┘
状态 含义 进入方式
NEW 新建,未 start new Thread()
RUNNABLE 可运行(可能正在执行,也可能在排 CPU 时间片) start()
BLOCKED 阻塞,等锁 synchronized 锁失败
WAITING 无限等待 wait() / park()
TIMED_WAITING 限时等待 sleep(1000) / wait(1000) / parkNanos()
TERMINATED 终止(执行完毕或异常退出) run() 结束

第三层:解密"JMM 内存黄金三角"(多线程的致命陷阱)

并发编程之所以难,根源在于 JMM(Java 内存模型)

CPU 核心 1          CPU 核心 2
┌──────────┐      ┌──────────┐
│ 工作内存  │      │ 工作内存  │
│ (L1/L2)  │      │ (L1/L2)  │
└────┬─────┘      └────┬─────┘
     │                  │
     └──────┬───────────┘
            │ 主内存 (Main Memory)
            ▼
      ┌────────────┐
      │  变量副本    │
      └────────────┘

线程不能直接操作主内存,必须先从大仓库把变量复制到自己的小口袋,改完了再写回大仓库。这套机制直接引爆了三大致命硬伤:

1. 可见性(Visibility)——幽灵变量陷阱

线程 A 改了变量但没写回主内存,或写回了但线程 B 还在读自己口袋里的老数据。

解法volatile——强制修改后立刻刷回主内存,并使其他线程的缓存行失效。

2. 原子性(Atomicity)——账目错乱陷阱

count++ 在 CPU 底层拆成三步:① 读 ② +1 ③ 写回。两步之间 CPU 切换给另一个线程,两次加法覆盖成一次。

解法:加锁(synchronized / ReentrantLock),或 AtomicInteger(基于 CAS)。

3. 有序性(Ordering)——编译器背刺陷阱

编译器和 CPU 为了性能,会把无依赖的指令重排。单线程下没事,多线程下可能导致拿到"还没初始化完毕的半成品对象"。

解法volatile 在底层插入内存屏障,禁止指令重排。


第四层:核心锁体系——两大王牌

王牌一:synchronized(重量级关键字、内置锁)

JDK 6 后引入锁升级机制,不再是纯重量级:

偏向锁(零开销,只在对象头记个线程 ID)
    ↓ 轻度竞争
轻量级锁/自旋锁(线程不挂起,死循环抢锁,避免上下文切换)
    ↓ 激烈竞争
重量级锁(交给操作系统内核,挂起线程)
// 修饰方法
public synchronized void doSomething() { }

// 修饰代码块
synchronized (this) { /* 临界区 */ }

王牌二:ReentrantLock(显式锁、JUC 核心)

纯 Java 代码(基于 AQS),能做 synchronized 做不到的操作:

ReentrantLock lock = new ReentrantLock();

// 尝试拿锁,5 秒抢不到就走人——彻底避免死锁
if (lock.tryLock(5, TimeUnit.SECONDS)) {
    try {
        // 临界区
    } finally {
        lock.unlock();
    }
}

两者对比

维度 synchronized ReentrantLock
身份 Java 关键字 JUC 类
底层 对象头 Mark Word + ObjectMonitor AQS(volatile state + CAS)
锁释放 自动释放(退出同步块) 必须手动 unlock()
超时 ❌ 不支持 tryLock(timeout)
公平锁 ❌ 非公平 ✅ 可配置公平/非公平
底层睡眠 Linux Futex LockSupport.park() → Futex

第五层:锁的三层金字塔(从硬件到操作系统到 JVM)

第一层:硬件印章——CPU 原子指令

CAS(Compare And Swap)lock cmpxchg 是 CPU 提供的一条不可分割的原子指令。

  • 锁总线(早期):发送 LOCK# 信号锁住整根内存总线,其他核心无法读写
  • 锁缓存(现代):利用 MESI 缓存一致性协议,只锁定该数据对应的缓存行(Cache Line),修改后通过硬件信号强制其他核心的缓存行失效

一切软件锁的终极基石:CPU 的 lock cmpxchg

第二层:操作系统大闸——Linux Futex

当 CAS 抢锁失败,线程不能一直自旋烧 CPU。JVM 向操作系统发起系统调用。

Futex(Fast Userspace Mutex)

  • 无竞争(用户态):直接用 CAS 拿锁,全程不打扰内核,性能极高
  • 有竞争(内核态):调用 futex(FUTEX_WAIT),内核把线程从 CPU 运行队列摘下,塞进等待队列(Wait Queue),线程进入真正睡眠(BLOCKED),不耗 CPU
  • 释放锁时:调用 futex(FUTEX_WAKE),内核唤醒等待队列里的线程,丢回 CPU 运行队列

第三层:Java 门面——Mark Word 与 AQS

synchronized 的底层:所有秘密在对象头(Mark Word)中:

锁状态 Mark Word 内容
偏向锁 54 位记录线程 ID,下次拿锁直接放行
轻量级锁 指向栈中锁记录的指针
重量级锁 标志位 10,指针指向 C++ 的 ObjectMonitor(内部维护 EntryList + WaitSet,依赖 Futex)

ReentrantLock 的底层:纯 Java 的 AQS(AbstractQueuedSynchronizer)

// 核心伪代码
if (compareAndSetState(0, 1)) {
    // CAS 抢锁成功!把 state 从 0 改成 1
    setExclusiveOwnerThread(Thread.currentThread());
} else {
    // 抢锁失败,封装成 Node 塞进 FIFO 双向链表队列
    acquireQueued(addWaiter(Node.EXCLUSIVE), arg);
    // 排队太久还抢不到 → LockSupport.park() → 底层 Futex
}

实战检测:反射修改 final Map 的并发灾难

场景:单例类 SystemConfig 内有 private final Map<String, String> internalRouteTable。线程 A 通过反射执行 field.set(config, new HashMap<>()) 强行替换引用。成百上千线程并发读写。

灾难一:可见性崩塌——"记忆幽灵"

final 的内存屏障只在构造函数结束时生效。反射强行修改引用后,JMM 不会插入任何新屏障

部分线程的工作内存里还死死保存着旧 Map 的地址,调用 getRouteTable() 时继续访问旧表。系统陷入"部分线程写新表,部分线程读旧表"的时空错乱。

灾难二:HashMap 并发写入——"二次投毒"

普通的 HashMap 根本不是线程安全的。并发 put() 会导致:

  • JDK 7 及以前:并发扩容时链表形成环形链表get() 触发死循环,CPU 飙到 100%
  • JDK 8 及以后:红黑树/链表指针覆盖,数据丢失;modCount 错乱引爆 ConcurrentModificationException

正确的工业做法

// 用 ConcurrentHashMap 替代 HashMap
private final Map<String, String> routeTable = new ConcurrentHashMap<>();

// 需要整体替换时,用 volatile 修饰引用
private volatile Map<String, String> routeTable = new ConcurrentHashMap<>();
// 替换时 atomic 操作
routeTable = new ConcurrentHashMap<>(newRoutes);

🏁 你建立起的 Java 并发系统化认知

  • 四大骨架:Thread / Runnable / Callable / ThreadPoolExecutor
  • 六种状态:NEW → RUNNABLE → BLOCKED / WAITING / TIMED_WAITING → TERMINATED
  • JMM 三大硬伤:可见性(volatile)/ 原子性(锁或 CAS)/ 有序性(内存屏障)
  • 锁升级:偏向锁 → 轻量级锁(自旋)→ 重量级锁(Futex)
  • 锁的三层金字塔:CPU lock cmpxchg → Linux Futex → JVM Mark Word / AQS

 

评论

此博客中的热门博文

我写了半年 prompt,最后发现最好的技巧就三个