引子:一个秒杀系统的困局

假设你正在开发一个电商秒杀系统,库存100件商品,瞬间涌入1000个并发请求。看似简单的需求,却隐藏着并发编程最本质的困难。

场景A:单线程实现(简单但性能差)

/**
 * 单线程秒杀服务
 * 优势:简单、可预测、无并发问题
 * 劣势:性能差,无法处理高并发
 */
public class SingleThreadSeckillService {
    private int stock = 100;  // 库存

    public synchronized boolean seckill(String userId) {
        // 检查库存
        if (stock <= 0) {
            System.out.println("库存不足,秒杀失败");
            return false;
        }

        // 模拟业务处理(数据库操作、支付调用等)
        try {
            Thread.sleep(10);  // 10ms的业务处理时间
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        // 扣减库存
        stock--;
        System.out.println("用户 " + userId + " 秒杀成功,剩余库存:" + stock);
        return true;
    }
}

// 性能测试
public class SingleThreadTest {
    public static void main(String[] args) {
        SingleThreadSeckillService service = new SingleThreadSeckillService();
        long start = System.currentTimeMillis();

        // 1000个用户顺序秒杀
        for (int i = 0; i < 1000; i++) {
            service.seckill("User-" + i);
        }

        long end = System.currentTimeMillis();
        System.out.println("总耗时:" + (end - start) + "ms");
    }
}

/*
执行结果:
总耗时:10,000ms(10秒)
分析:每个请求10ms,1000个请求串行执行,总计10秒
问题:在真实秒杀场景中,10秒是不可接受的响应时间
*/

性能瓶颈分析

单线程处理能力:
├─ 单个请求耗时:10ms
├─ QPS(每秒请求数):1000ms / 10ms = 100 QPS
├─ 1000个请求耗时:1000 × 10ms = 10秒
└─ 结论:无法满足秒杀场景的高并发需求(需要>10000 QPS)

场景B:多线程实现(性能好但问题多)

/**
 * 多线程秒杀服务(错误示范)
 * 优势:性能大幅提升
 * 劣势:存在严重的并发Bug
 */
public class MultiThreadSeckillService {
    private int stock = 100;  // 库存(共享变量)

    public boolean seckill(String userId) {
        // 检查库存(问题1:可见性问题)
        if (stock <= 0) {
            System.out.println("库存不足,秒杀失败");
            return false;
        }

        // 模拟业务处理
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        // 扣减库存(问题2:原子性问题)
        stock--;  // 这不是原子操作!
        System.out.println("用户 " + userId + " 秒杀成功,剩余库存:" + stock);
        return true;
    }
}

// 多线程性能测试
public class MultiThreadTest {
    public static void main(String[] args) throws InterruptedException {
        MultiThreadSeckillService service = new MultiThreadSeckillService();
        CountDownLatch latch = new CountDownLatch(1000);

        long start = System.currentTimeMillis();

        // 1000个线程并发秒杀
        for (int i = 0; i < 1000; i++) {
            final int userId = i;
            new Thread(() -> {
                service.seckill("User-" + userId);
                latch.countDown();
            }).start();
        }

        latch.await();  // 等待所有线程完成
        long end = System.currentTimeMillis();
        System.out.println("总耗时:" + (end - start) + "ms");
    }
}

/*
执行结果(多次运行结果不一致):
总耗时:50ms(相比单线程提升200倍!)
但是...

问题1:库存超卖
- 期望:100件商品,只有前100个用户秒杀成功
- 实际:可能有150个用户秒杀成功(库存变成-50)

问题2:最终库存不一致
- 第一次运行:最终库存 = -45
- 第二次运行:最终库存 = -52
- 第三次运行:最终库存 = -38
每次结果都不同!

问题3:可见性问题
- 有些线程读到的stock是过期值
- 线程A扣减库存后,线程B可能看不到最新值
*/

并发Bug剖析

// 问题的本质:stock--不是原子操作
// stock-- 实际上分为三步:
// 1. 读取stock的值(READ)
// 2. 计算stock - 1(COMPUTE)
// 3. 写回stock(WRITE)

// 并发场景下的交错执行:
时间线
t1: 线程A读取stock = 100
t2: 线程B读取stock = 100  A还没写回
t3: 线程A计算 100 - 1 = 99
t4: 线程B计算 100 - 1 = 99  B也读到了100
t5: 线程A写回 stock = 99
t6: 线程B写回 stock = 99  覆盖了A的结果!)

结果两个线程都秒杀成功但库存只减少了1应该减少2

场景C:正确的多线程实现

/**
 * 多线程秒杀服务(正确版本)
 * 使用synchronized保证线程安全
 */
public class SafeMultiThreadSeckillService {
    private int stock = 100;

    public synchronized boolean seckill(String userId) {
        // 检查库存
        if (stock <= 0) {
            System.out.println("库存不足,秒杀失败");
            return false;
        }

        // 模拟业务处理
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        // 扣减库存
        stock--;
        System.out.println("用户 " + userId + " 秒杀成功,剩余库存:" + stock);
        return true;
    }
}

/*
执行结果:
总耗时:1,000ms(1秒)
库存最终值:0(正确!)
成功用户数:100(正确!)

性能对比:
├─ 单线程:10,000ms
├─ 多线程(错误):50ms(但结果错误)
└─ 多线程(正确):1,000ms(结果正确,性能提升10倍)

结论:
虽然synchronized引入了同步开销,但相比单线程仍有10倍性能提升
关键是:正确性 > 性能
*/

三种方案的对比

对比维度单线程多线程(错误)多线程(正确)
耗时10,000ms50ms1,000ms
QPS10020,0001,000
正确性✅ 正确❌ 库存超卖✅ 正确
可预测性✅ 结果一致❌ 每次不同✅ 结果一致
代码复杂度简单简单(但有隐藏Bug)中等
调试难度容易极难(Bug难复现)中等
适用场景低并发❌ 不可用高并发

核心洞察

并发编程的本质困难:
├─ 性能与正确性的权衡
├─ 简单性与复杂性的权衡
└─ Bug的隐蔽性和难以复现

并发编程的黄金法则:
正确性 > 性能 > 简单性
宁愿慢一点,也不能出错

一、并发的本质困难

1.1 第一性原理拆解

并发编程的三大核心问题

为什么并发如此困难?
└─ 共享内存的多线程模型带来三大问题

问题1:可见性(Visibility)
└─ 一个线程对共享变量的修改,其他线程能否立即看到?

问题2:原子性(Atomicity)
└─ 一个操作是否不可分割?能否被其他线程打断?

问题3:有序性(Ordering)
└─ 程序执行的顺序是否与代码顺序一致?

1.2 可见性问题:CPU缓存的困扰

1.2.1 问题根源:多级缓存架构

现代计算机的内存架构:

CPU Core 1              CPU Core 2
   ↓                       ↓
L1 Cache (32KB)        L1 Cache (32KB)   ← 每个核心独立
   ↓                       ↓
L2 Cache (256KB)       L2 Cache (256KB)  ← 每个核心独立
   ↓                       ↓
        L3 Cache (8MB)                    ← 所有核心共享
              ↓
         主内存 (RAM)

性能差异(访问延迟):
├─ L1 Cache: 1ns
├─ L2 Cache: 3ns
├─ L3 Cache: 12ns
├─ 主内存: 100ns
└─ 相差100倍!

设计目标:用缓存隐藏内存延迟
副作用:带来可见性问题

1.2.2 可见性问题的实际案例

/**
 * 可见性问题演示
 * 场景:线程A修改flag,线程B循环检查flag
 * 问题:B可能永远看不到A的修改
 */
public class VisibilityProblem {
    private static boolean flag = false;  // 共享变量

    public static void main(String[] args) throws InterruptedException {
        // 线程A:1秒后修改flag
        Thread threadA = new Thread(() -> {
            try {
                Thread.sleep(1000);
                flag = true;  // 修改flag
                System.out.println("线程A:flag已设置为true");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        // 线程B:循环检查flag
        Thread threadB = new Thread(() -> {
            while (!flag) {
                // 空循环,等待flag变为true
            }
            System.out.println("线程B:检测到flag = true,退出循环");
        });

        threadB.start();
        Thread.sleep(100);  // 确保B先启动
        threadA.start();

        threadA.join();
        threadB.join();
    }
}

/*
执行结果:
线程A:flag已设置为true
(然后程序卡住,线程B永远不会退出)

问题分析:
1. 线程B读取flag并缓存到CPU寄存器
2. 循环中一直读取寄存器的值(false)
3. 线程A修改的flag在主内存,B看不到
4. 即使A写入主内存,B的缓存也不会自动失效

解决方案1:使用volatile
private static volatile boolean flag = false;

解决方案2:使用synchronized
synchronized(lock) { flag = true; }
*/

可见性问题的本质

单线程视角(程序员期望):
写变量 → 立即生效 → 其他线程可见

多线程实际情况:
线程A写变量 → 写入L1缓存 → 刷新到主内存(延迟)
线程B读变量 ← 读L1缓存(可能是旧值) ← 主内存

时间差问题:
├─ A写入缓存:t1
├─ A刷新到主内存:t2(t2 > t1)
├─ B的缓存失效:t3(t3 > t2)
└─ B读取新值:t4(t4 > t3)

期望:t1 = t2 = t3 = t4(立即可见)
实际:t1 < t2 < t3 < t4(存在延迟)

1.3 原子性问题:指令交错执行

1.3.1 问题根源:CPU的指令级并行

高级语言的一行代码 ≠ CPU的一条指令

示例:stock--;

Java代码(1行):
stock--;

字节码(3条指令):
1. getfield stock      // 读取stock的值
2. iconst_1            // 常量1入栈
3. isub                // 减法运算
4. putfield stock      // 写回stock

CPU指令(更多):
1. MOV EAX, [stock]    // 从内存读取stock到寄存器EAX
2. DEC EAX             // EAX减1
3. MOV [stock], EAX    // 从寄存器EAX写回内存

关键问题:这些指令不是原子的!
线程切换可能发生在任何两条指令之间

1.3.2 原子性问题的实际案例

/**
 * 原子性问题演示
 * 场景:1000个线程并发累加counter
 * 期望:最终结果10000
 * 实际:结果小于10000(每次不同)
 */
public class AtomicityProblem {
    private static int counter = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[1000];

        for (int i = 0; i < 1000; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 10; j++) {
                    counter++;  // 非原子操作
                }
            });
        }

        // 启动所有线程
        for (Thread thread : threads) {
            thread.start();
        }

        // 等待所有线程完成
        for (Thread thread : threads) {
            thread.join();
        }

        System.out.println("最终结果:" + counter);
        System.out.println("期望结果:10000");
        System.out.println("丢失更新:" + (10000 - counter));
    }
}

/*
执行结果(多次运行):
第1次:最终结果:9876  丢失更新:124
第2次:最终结果:9923  丢失更新:77
第3次:最终结果:9851  丢失更新:149

问题分析:
counter++ 分解为三步:
1. 读取counter(READ)
2. 加1(ADD)
3. 写回counter(WRITE)

并发场景下的交错执行:
时间  线程A              线程B
t1   READ(0)
t2                      READ(0)
t3   ADD(0+1=1)
t4                      ADD(0+1=1)
t5   WRITE(1)
t6                      WRITE(1)  ← 覆盖了A的结果!

结果:两个线程都执行了counter++,但counter只增加了1
*/

原子性问题的可视化

预期的原子执行(理想情况):
线程A: [READ → ADD → WRITE]
线程B:                      [READ → ADD → WRITE]

实际的交错执行(问题场景):
线程A: [READ → ADD →               WRITE]
线程B:            [READ → ADD → WRITE]
                  ↑
                  这里读到了过期值

丢失更新的根本原因:
READ和WRITE之间有时间窗口,其他线程可能在此期间修改变量

1.4 有序性问题:指令重排序

1.4.1 问题根源:编译器和CPU的优化

为什么会发生指令重排序?

层级1:编译器重排序
├─ Java源码 → 字节码
├─ 编译器会调整指令顺序以优化性能
└─ 前提:不改变单线程的执行结果

层级2:CPU指令重排序
├─ CPU乱序执行(Out-of-Order Execution)
├─ CPU流水线优化
└─ 前提:不违反as-if-serial语义

层级3:内存系统重排序
├─ 写缓冲区(Store Buffer)
├─ 失效队列(Invalidate Queue)
└─ 前提:单处理器内存模型一致

问题:单线程正确 ≠ 多线程正确
编译器和CPU只保证单线程语义,不考虑多线程

1.4.2 有序性问题的经典案例:DCL单例

/**
 * 双重检查锁定(DCL)单例模式
 * 问题:存在指令重排序导致的线程安全问题
 */
public class Singleton {
    private static Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {                    // 第一次检查
            synchronized (Singleton.class) {
                if (instance == null) {            // 第二次检查
                    instance = new Singleton();    // 问题根源!
                }
            }
        }
        return instance;
    }
}

/*
问题分析:
instance = new Singleton() 分解为三步:

1. memory = allocate()    // 分配内存
2. ctorInstance(memory)   // 初始化对象
3. instance = memory      // 设置instance指向内存地址

指令重排序后可能变成:
1. memory = allocate()    // 分配内存
3. instance = memory      // 设置instance指向内存地址(提前!)
2. ctorInstance(memory)   // 初始化对象

并发问题:
时间  线程A                           线程B
t1   分配内存
t2   instance = memory(未初始化)
t3                                   if (instance == null) → false
t4                                   return instance  ← 返回未初始化的对象!
t5   初始化对象

结果:线程B拿到了一个未初始化的对象,使用时会出错

解决方案1:使用volatile禁止重排序
private static volatile Singleton instance;

解决方案2:使用静态内部类(推荐)
private static class SingletonHolder {
    private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
    return SingletonHolder.INSTANCE;
}
*/

有序性问题的本质

单线程视角(as-if-serial):
代码顺序:A → B → C
执行顺序:A → B → C(或 A → C → B,只要结果一致)
保证:最终结果与代码顺序执行一致

多线程视角:
线程1:A1 → B1
线程2:A2 → B2
可能的交错执行:
├─ A1 → A2 → B1 → B2
├─ A1 → B1 → A2 → B2
├─ A2 → A1 → B2 → B1
└─ ...(多种可能)

指令重排序导致的问题:
线程1:A1 → C1 → B1  (B1和C1重排序)
线程2:A2 → B2
交错执行:A1 → C1 → A2 → B2 → B1
如果B2依赖C1的副作用,就会出错

二、Java内存模型(JMM)

2.1 为什么需要内存模型?

核心问题:如何在保证性能的同时,提供一致的并发语义?

矛盾的需求:
├─ 硬件:追求性能,使用缓存、乱序执行、预测执行
├─ 编译器:追求优化,进行指令重排序
└─ 程序员:追求正确性,期望代码按顺序执行

Java内存模型(JMM)的角色:
在硬件/编译器的优化和程序员的正确性期望之间,建立一个契约
├─ 定义:什么样的程序行为是合法的
├─ 约束:编译器和CPU可以做哪些优化
└─ 保证:在遵守规则的前提下,程序员能得到什么保证

2.2 JMM的抽象结构

Java内存模型的抽象:

线程A                      线程B
  ↓                          ↓
工作内存A                  工作内存B
├─ 本地变量               ├─ 本地变量
├─ 共享变量副本           ├─ 共享变量副本
└─ 操作缓存               └─ 操作缓存
  ↓                          ↓
        主内存(共享变量)
        ├─ 实例字段
        ├─ 静态字段
        └─ 数组元素

关键理解:
1. 所有共享变量存储在主内存
2. 每个线程有自己的工作内存(抽象概念,映射到寄存器、缓存、写缓冲区)
3. 线程对变量的操作必须在工作内存中进行
4. 不同线程之间无法直接访问对方的工作内存
5. 线程间通信必须通过主内存完成

与硬件的映射关系:
工作内存 ≈ CPU寄存器 + L1/L2/L3缓存 + 写缓冲区
主内存 ≈ 物理内存(RAM)

2.3 happens-before规则:JMM的核心

happens-before的定义

如果操作A happens-before 操作B,那么:
1. A的执行结果对B可见
2. A的执行顺序在B之前

关键理解:
├─ happens-before不是"时间上的先后"(物理时序)
├─ 而是"可见性和有序性的保证"(逻辑时序)
└─ 即使A在时间上晚于B执行,只要满足happens-before规则,程序行为仍然正确

2.3.1 happens-before的八大规则

/**
 * 规则1:程序顺序规则(Program Order Rule)
 * 一个线程内,代码的执行顺序,书写在前面的操作 happens-before 书写在后面的操作
 */
int a = 1;  // A
int b = 2;  // B
// A happens-before B
// 保证:B能看到A对a的写入

/**
 * 规则2:监视器锁规则(Monitor Lock Rule)
 * 对一个锁的解锁 happens-before 随后对这个锁的加锁
 */
synchronized (lock) {
    // A:释放锁之前的所有操作
    x = 1;
}  // 解锁

synchronized (lock) {
    // B:加锁之后的所有操作
    int y = x;  // 能看到 x = 1
}

/**
 * 规则3:volatile变量规则(Volatile Variable Rule)
 * 对一个volatile变量的写 happens-before 任意后续对这个volatile变量的读
 */
volatile boolean flag = false;

// 线程A
x = 1;              // 普通写
flag = true;        // volatile写

// 线程B
if (flag) {         // volatile读
    int y = x;      // 能看到 x = 1
}

/**
 * 规则4:传递性规则(Transitivity)
 * 如果 A happens-before B,B happens-before C,那么 A happens-before C
 */
x = 1;              // A
flag = true;        // B(volatile写)
if (flag) {         // C(volatile读)
    int y = x;      // A happens-before C,能看到 x = 1
}

/**
 * 规则5:线程启动规则(Thread Start Rule)
 * Thread.start() happens-before 该线程的每一个操作
 */
int x = 0;
Thread t = new Thread(() -> {
    int y = x;  // 能看到主线程对x的修改
});
x = 1;
t.start();  // start() happens-before 线程内的操作

/**
 * 规则6:线程终止规则(Thread Termination Rule)
 * 线程的所有操作 happens-before 其他线程检测到该线程终止
 * (通过Thread.join()或Thread.isAlive())
 */
Thread t = new Thread(() -> {
    x = 1;  // 线程内操作
});
t.start();
t.join();  // 等待线程终止
int y = x;  // 能看到 x = 1

/**
 * 规则7:线程中断规则(Thread Interruption Rule)
 * 对线程interrupt()的调用 happens-before 被中断线程检测到中断事件
 */
Thread t = new Thread(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        // 能检测到中断
    }
});
t.start();
t.interrupt();  // happens-before 线程内检测到中断

/**
 * 规则8:对象终结规则(Finalizer Rule)
 * 一个对象的初始化完成 happens-before 它的finalize()方法的开始
 */
// 保证对象构造完成后才能执行finalize()

2.4 volatile的语义和实现

2.4.1 volatile的两大语义

/**
 * volatile的语义
 * 1. 可见性:对volatile变量的写,立即刷新到主内存;读,从主内存读取
 * 2. 有序性:禁止指令重排序
 */
public class VolatileSemantics {
    private int a = 0;
    private volatile boolean flag = false;

    // 线程A
    public void writer() {
        a = 1;              // 1
        flag = true;        // 2(volatile写)
    }

    // 线程B
    public void reader() {
        if (flag) {         // 3(volatile读)
            int i = a;      // 4
            // i一定等于1
        }
    }
}

/*
happens-before分析:
1. 程序顺序规则:1 happens-before 2
2. volatile规则:2 happens-before 3
3. 程序顺序规则:3 happens-before 4
4. 传递性:1 happens-before 4

结论:操作4能看到操作1的结果

禁止重排序的规则:
volatile写之前的操作 → 不能重排序到volatile写之后
volatile读之后的操作 → 不能重排序到volatile读之前
*/

2.4.2 volatile的底层实现:内存屏障

volatile的实现机制:内存屏障(Memory Barrier)

内存屏障的四种类型:
1. LoadLoad屏障:   Load1; LoadLoad; Load2
   确保Load1数据的装载先于Load2及后续装载指令

2. StoreStore屏障: Store1; StoreStore; Store2
   确保Store1数据对其他处理器可见(刷新到主内存)先于Store2及后续存储指令

3. LoadStore屏障:  Load1; LoadStore; Store2
   确保Load1数据装载先于Store2及后续存储指令刷新到主内存

4. StoreLoad屏障:  Store1; StoreLoad; Load2
   确保Store1数据对其他处理器可见先于Load2及后续装载指令
   最昂贵的屏障,会使该屏障之前的所有内存访问指令完成

volatile写插入的内存屏障:
StoreStore屏障
volatile写操作
StoreLoad屏障

volatile读插入的内存屏障:
volatile读操作
LoadLoad屏障
LoadStore屏障

示例:
int a = 1;              // 普通写
StoreStore屏障          // 防止上面的普通写与下面的volatile写重排序
volatile写 flag = true
StoreLoad屏障           // 防止上面的volatile写与下面的操作重排序

volatile读 flag
LoadLoad屏障            // 防止下面的普通读与上面的volatile读重排序
LoadStore屏障           // 防止下面的普通写与上面的volatile读重排序
int b = a;              // 普通读

2.4.3 volatile的使用场景

/**
 * 场景1:状态标志
 * 最典型的用法
 */
public class StatusFlag {
    private volatile boolean shutdownRequested = false;

    public void shutdown() {
        shutdownRequested = true;
    }

    public void doWork() {
        while (!shutdownRequested) {
            // 做业务逻辑
        }
    }
}

/**
 * 场景2:DCL单例模式
 * 防止指令重排序导致的问题
 */
public class Singleton {
    private static volatile Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

/**
 * 场景3:独立观察(发布-订阅)
 * 一个线程写,多个线程读
 */
public class CurrentTemperature {
    private volatile int temperature;

    public void update(int newTemp) {
        temperature = newTemp;  // 写线程
    }

    public int read() {
        return temperature;  // 读线程
    }
}

/**
 * 注意:volatile不能保证复合操作的原子性
 */
public class VolatileCounter {
    private volatile int count = 0;

    public void increment() {
        count++;  // 非原子操作!仍然有线程安全问题
    }

    // 正确做法1:使用synchronized
    public synchronized void incrementSafe1() {
        count++;
    }

    // 正确做法2:使用AtomicInteger
    private AtomicInteger count2 = new AtomicInteger(0);
    public void incrementSafe2() {
        count2.incrementAndGet();
    }
}

2.5 final的内存语义

/**
 * final的内存语义
 * 1. 构造函数内对final字段的写入,与把this引用赋值给其他变量,不能重排序
 * 2. 初次读对象引用,与初次读该对象包含的final字段,不能重排序
 */
public class FinalFieldExample {
    final int x;
    int y;
    static FinalFieldExample obj;

    public FinalFieldExample() {
        x = 1;  // final字段的写入
        y = 2;  // 普通字段的写入
    }

    // 线程A
    public static void writerThread() {
        obj = new FinalFieldExample();
    }

    // 线程B
    public static void readerThread() {
        FinalFieldExample local = obj;  // 读对象引用
        if (local != null) {
            int a = local.x;  // 一定能看到 x = 1
            int b = local.y;  // 可能看到 y = 0(普通字段)
        }
    }
}

/*
final的保证:
1. 在构造函数内对final字段的写入,不会与构造函数外的对象引用赋值重排序
2. 一旦在构造函数内初始化final字段,其他线程就能看到final字段的值
3. 普通字段没有这个保证

错误示例(如果x不是final):
可能的重排序:
obj = new FinalFieldExample();  // 对象引用赋值
x = 1;                          // 字段初始化

如果发生重排序,线程B可能看到 obj != null 但 x = 0
*/

三、并发Bug剖析

3.1 经典案例1:双重检查锁定问题

我们在2.4.2节已经分析过DCL单例的问题,这里进行更深入的剖析。

/**
 * 问题版本:没有volatile
 */
public class Singleton {
    private static Singleton instance;  // 问题:没有volatile

    public static Singleton getInstance() {
        if (instance == null) {                    // 1. 第一次检查
            synchronized (Singleton.class) {       // 2. 加锁
                if (instance == null) {            // 3. 第二次检查
                    instance = new Singleton();    // 4. 创建对象
                }
            }
        }
        return instance;
    }
}

/*
深度剖析:为什么会出问题?

1. 对象创建的三个步骤:
   memory = allocate()    // 1. 分配内存
   ctorInstance(memory)   // 2. 初始化对象
   instance = memory      // 3. 设置instance指向内存

2. 可能的重排序:
   memory = allocate()    // 1. 分配内存
   instance = memory      // 3. 设置instance(提前!)
   ctorInstance(memory)   // 2. 初始化对象(延后!)

3. 并发问题:
   时间  线程A                           线程B
   t1   if (instance == null) → true
   t2   synchronized (Singleton.class)
   t3   if (instance == null) → true
   t4   memory = allocate()
   t5   instance = memory(未初始化)
   t6                                   if (instance == null) → false
   t7                                   return instance  ← 未初始化!
   t8   ctorInstance(memory)

4. 问题本质:
   线程B拿到的instance不为null,但指向的对象未完成初始化
   使用这个对象会导致不可预测的错误

5. 解决方案的对比:
*/

// 方案1:使用volatile(推荐)
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();
                    // volatile禁止重排序,保证初始化完成后才赋值
                }
            }
        }
        return instance;
    }
}

// 方案2:静态内部类(最推荐)
public class Singleton {
    private Singleton() {}

    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}
/*
优势:
1. 懒加载:只有调用getInstance()时才加载SingletonHolder
2. 线程安全:类加载机制保证初始化只执行一次
3. 无需同步:没有synchronized开销
4. 无需volatile:没有指令重排序问题
*/

// 方案3:枚举(最简洁)
public enum Singleton {
    INSTANCE;

    public void doSomething() {
        // 业务逻辑
    }
}
/*
优势:
1. 线程安全:枚举的实例创建是线程安全的
2. 防止反射攻击:枚举不能通过反射创建新实例
3. 防止反序列化攻击:枚举的反序列化会返回同一实例
4. 代码最简洁
*/

3.2 经典案例2:SimpleDateFormat的线程安全问题

/**
 * 问题:SimpleDateFormat不是线程安全的
 */
public class DateFormatProblem {
    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(10);

        for (int i = 0; i < 100; i++) {
            executor.submit(() -> {
                try {
                    String dateStr = "2024-01-15 12:30:00";
                    Date date = sdf.parse(dateStr);
                    System.out.println(date);
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            });
        }

        executor.shutdown();
    }
}

/*
执行结果(多次运行可能出现不同问题):
1. java.lang.NumberFormatException
2. 解析出错误的日期
3. 偶尔正常

问题根源:
SimpleDateFormat内部使用Calendar对象,calendar字段是共享的
多线程并发调用parse()方法,会同时修改calendar,导致数据混乱

源码分析:
public Date parse(String source) throws ParseException {
    // calendar是成员变量,多线程共享
    calendar.clear();           // 清空日历
    calendar.set(...);          // 设置日期
    // 问题:线程A执行到一半,线程B清空了calendar
}

解决方案对比:
*/

// 方案1:每次创建新实例(浪费资源)
public class Solution1 {
    public Date parse(String dateStr) throws ParseException {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return sdf.parse(dateStr);
    }
}

// 方案2:使用synchronized(性能差)
public class Solution2 {
    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public synchronized Date parse(String dateStr) throws ParseException {
        return sdf.parse(dateStr);
    }
}

// 方案3:使用ThreadLocal(推荐)
public class Solution3 {
    private static final ThreadLocal<SimpleDateFormat> threadLocal = ThreadLocal.withInitial(
        () -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
    );

    public Date parse(String dateStr) throws ParseException {
        return threadLocal.get().parse(dateStr);
    }
}

// 方案4:使用DateTimeFormatter(JDK 8+,最推荐)
public class Solution4 {
    private static final DateTimeFormatter formatter =
        DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    public LocalDateTime parse(String dateStr) {
        return LocalDateTime.parse(dateStr, formatter);
    }
}
/*
优势:
1. 线程安全:DateTimeFormatter是不可变类
2. 性能好:无需同步,无需创建新实例
3. API更友好:新的时间API更好用
*/

3.3 如何排查并发Bug

并发Bug的特点:
├─ 难以复现:可能运行1000次才出现1次
├─ 难以调试:加了日志或断点,Bug可能消失(Heisenbug)
├─ 难以定位:错误现场可能离问题根源很远
└─ 危害严重:数据不一致、死锁、内存泄漏

排查方法:

1. 静态代码分析
   ├─ 工具:FindBugs、PMD、SonarQube
   ├─ 检查:共享变量未同步、双重检查锁定错误
   └─ 局限:只能发现明显的问题

2. 动态分析工具
   ├─ JProfiler:监控线程状态、死锁检测
   ├─ VisualVM:线程dump分析
   ├─ Arthas:阿里开源的Java诊断工具
   └─ ThreadSanitizer:Google的动态数据竞争检测器

3. 日志分析
   ├─ 线程ID:记录每个操作的线程ID
   ├─ 时间戳:精确到微秒
   ├─ 操作序列:记录关键操作的顺序
   └─ 变量状态:记录共享变量的变化

4. 压力测试
   ├─ 高并发:使用多线程模拟高并发场景
   ├─ 长时间运行:运行数小时甚至数天
   ├─ 随机化:引入随机延迟,增加交错执行的概率
   └─ 重复运行:同样的测试运行1000次

5. 代码审查
   ├─ 关注点:共享变量、锁的范围、wait/notify
   ├─ 问题点:双重检查锁定、SimpleDateFormat
   └─ 最佳实践:不可变对象、线程本地存储

四、总结与思考

4.1 并发编程的核心原则

1. 优先使用不可变对象
   ├─ 不可变对象天然线程安全
   ├─ String、Integer等包装类都是不可变的
   └─ 自定义类:所有字段final + 不提供setter

2. 减少共享变量
   ├─ 局部变量优于成员变量
   ├─ ThreadLocal实现线程隔离
   └─ 栈封闭:数据只在方法内使用

3. 同步必不可少时,选择合适的工具
   ├─ synchronized:简单场景
   ├─ Lock:需要灵活性(超时、可中断、条件变量)
   ├─ Atomic类:简单的原子操作
   └─ 并发集合:ConcurrentHashMap、BlockingQueue

4. 最小化锁的范围
   ├─ 锁的代码越少越好
   ├─ 避免在锁内调用外部方法
   └─ 避免嵌套锁

5. 性能与正确性的权衡
   ├─ 正确性 > 性能
   ├─ 先保证正确,再优化性能
   └─ 不要过早优化

4.2 何时使用并发,何时避免并发

适合使用并发的场景:
├─ CPU密集型任务:多核并行计算
├─ IO密集型任务:网络请求、文件读写
├─ 响应时间要求高:秒杀、实时系统
└─ 吞吐量要求高:服务器处理大量请求

不适合使用并发的场景:
├─ 简单任务:单线程足够快
├─ 任务间有强依赖:无法并行
├─ 共享状态多:同步开销大于收益
└─ 开发时间紧:并发Bug难以排查

并发的代价:
├─ 开发成本:代码复杂度提升
├─ 调试成本:Bug难以复现和定位
├─ 性能开销:上下文切换、同步开销
└─ 心智负担:需要理解并发模型

4.3 并发编程的学习路径

阶段1:理解并发的本质问题(本文)
├─ 可见性、原子性、有序性
├─ Java内存模型(JMM)
├─ happens-before规则
└─ volatile、final的语义

阶段2:掌握同步机制(下一篇)
├─ synchronized原理
├─ Lock接口与ReentrantLock
├─ AQS(AbstractQueuedSynchronizer)
└─ 死锁问题

阶段3:并发工具类(第三篇)
├─ Atomic原子类
├─ CAS原理
├─ CountDownLatch、CyclicBarrier、Semaphore
└─ 无锁编程

阶段4:线程池与异步编程(第四篇)
├─ ThreadPoolExecutor
├─ ForkJoinPool
├─ CompletableFuture
└─ 异步编程模式

阶段5:并发集合(第五篇)
├─ ConcurrentHashMap
├─ BlockingQueue
├─ CopyOnWriteArrayList
└─ 高性能并发集合

阶段6:设计模式与最佳实践(第六篇)
├─ 不可变对象模式
├─ ThreadLocal
├─ 两阶段终止
└─ 并发编程最佳实践

五、下一篇预告

在下一篇文章《线程安全与同步机制:从synchronized到Lock》中,我们将深入探讨:

  1. 什么是线程安全?

    • 线程安全的三种实现方式
    • 不可变对象的威力
  2. synchronized深度解析

    • 对象头与Monitor
    • 锁升级过程(偏向锁、轻量级锁、重量级锁)
    • wait/notify机制
  3. Lock接口与ReentrantLock

    • 为什么需要Lock?
    • 公平锁 vs 非公平锁
    • Condition条件队列
  4. AQS源码剖析

    • AQS的核心思想
    • 同步队列(CLH锁队列)
    • 手写一个基于AQS的同步器
  5. 死锁问题

    • 死锁的四个必要条件
    • 死锁预防策略
    • 死锁检测工具

敬请期待!


本文要点回顾

核心问题:
├─ 可见性:CPU缓存导致变量修改不可见
├─ 原子性:指令交错执行导致复合操作被打断
└─ 有序性:编译器和CPU重排序导致执行顺序与代码顺序不一致

Java内存模型(JMM):
├─ 抽象结构:主内存 + 工作内存
├─ happens-before规则:8大规则保证可见性和有序性
├─ volatile:可见性 + 禁止重排序
└─ final:构造函数内的写入对其他线程可见

经典Bug:
├─ 双重检查锁定:指令重排序导致返回未初始化对象
├─ SimpleDateFormat:共享Calendar导致线程不安全
└─ 排查方法:静态分析、动态工具、压力测试

核心原则:
├─ 正确性 > 性能
├─ 优先使用不可变对象
├─ 减少共享变量
└─ 选择合适的同步工具

系列文章

  1. Java并发第一性原理:为什么并发如此困难?(本文)
  2. ⏳ 线程安全与同步机制:从synchronized到Lock
  3. ⏳ 并发工具类与原子操作:无锁编程的艺术
  4. ⏳ 线程池与异步编程:从Thread到CompletableFuture
  5. ⏳ 并发集合:从HashMap到ConcurrentHashMap
  6. ⏳ 并发设计模式与最佳实践

参考资料

  • 《Java并发编程实战》- Brian Goetz
  • 《Java并发编程的艺术》- 方腾飞、魏鹏、程晓明
  • Java Language Specification - Chapter 17: Threads and Locks
  • JSR-133: Java Memory Model and Thread Specification

关于作者: 专注于Java技术栈的深度研究,致力于用第一性原理思维拆解复杂技术问题。本系列文章采用渐进式复杂度模型,从"为什么"出发,系统化学习Java并发编程。

如果这篇文章对你有帮助,欢迎关注本系列后续文章!