LESSON 10 · 并发编程

Java 并发编程
线程 / 锁 / 线程池

这节啃 Java 后端第二个大厂考点:并发编程。从最基础的 Threadsynchronized 锁升级、再到 ThreadPoolExecutor 线程池——写 Java 后端绕不开

学完这节:你能解释清楚 synchronized 锁升级过程、说出 volatilesynchronized 的本质区别、能正确配置一个生产级线程池、知道大厂面试爱问的"AQS、ThreadLocal、CAS"是怎么回事。
1

进程 vs 线程:为什么要并发?

💡 动机:单线程不够用,才需要多线程
对比进程线程
内存 独立内存空间 ⭐ 共享进程内存
开销 大(创建慢、切换慢) ⭐ 小(轻量)
通信 进程间通信(IPC)复杂 ⭐ 直接共享变量
类比 一个工厂(独立) 工厂里的工人(协作)
为什么需要并发?
  • CPU 多核 → 多线程能榨干硬件
  • I/O 阻塞(数据库 / 网络)→ 用线程切换来掩盖等待
  • 接口响应时间 → 异步处理提高吞吐
2

创建线程的 3 种方式

💡 动机:先把"怎么开线程"会了,再谈线程安全

方式 1:继承 Thread

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("线程在跑:" + Thread.currentThread().getName());
    }
}
new MyThread().start();   // start() 才真正开线程,run() 只是普通方法

方式 2:实现 Runnable(推荐)

// 推荐:Java 单继承,Runnable 不占继承位
Runnable task = () -> System.out.println("hello from " + Thread.currentThread().getName());
new Thread(task).start();

方式 3:实现 Callable(拿返回值 / 抛异常)

// 区别:Callable 有返回值 + 能抛 checked 异常
Callable<Integer> task = () -> {
    Thread.sleep(1000);
    return 42;
};
FutureTask<Integer> ft = new FutureTask<>(task);
new Thread(ft).start();
Integer result = ft.get();   // 阻塞等结果
⭐ 必背口诀:
  • 拿返回值 → Callable + Future
  • 只想执行任务 → Runnable + Thread
  • 生产环境别直接 new Thread,用线程池(下文)
3

线程 6 种状态

💡 动机:看线程 dump / 排查卡死时必看状态
// Thread.State 枚举的 6 个值
public enum State {
    NEW,              // 刚 new 完,没 start
    RUNNABLE,         // 正在 JVM 里跑(CPU 上 or 等 CPU)
    BLOCKED,          // 阻塞在 synchronized 锁
    WAITING,          // 无限等待(Object.wait / Thread.join)
    TIMED_WAITING,    // 限时等待(sleep / wait(timeout))
    TERMINATED        // 跑完了

状态转换图(⭐ 面试常画)

       new Thread()              start()
           │                        │
           ▼                        ▼
         NEW ──────────────────▶ RUNNABLE ◀──┐
                                       │      │ CPU 调度
                            ┌──────────┤      │
                            │          ▼      │
              ┌─────────────┘      拿到锁     │
              │  sleep/join/wait              │
              ▼                                │
        TIMED_WAITING / WAITING ─────────────┘
              │  超时 / notify
              ▼
         BLOCKED(抢锁失败)
              │  拿到锁
              ▼
         RUNNABLE ──── run() 结束 ──▶ TERMINATED
4

synchronized + 锁升级 ⭐⭐ 面试必问

💡 动机:锁是 Java 后端最容易出 bug 的地方,也是面试官最爱问的

3 种用法

// 1. 修饰实例方法(锁 = this 对象)
public synchronized void save() { ... }

// 2. 修饰静态方法(锁 = Class 对象)
public static synchronized void create() { ... }

// 3. 修饰代码块(锁 = 括号里的对象)
public void update() {
    synchronized (this) {        // 等价于方法 1
        ...
    }
    synchronized (SomeClass.class) {  // 等价于方法 2
        ...
    }
}

⭐⭐ 锁升级(Java 6+ 优化)

JDK 6 之前 synchronized = 直接 OS 互斥锁,很重。JDK 6+ 优化成"渐进式升级":

锁级别触发条件开销
⭐ 偏向锁 ⭐ 只有 1 个线程访问 ⭐ 几乎无开销(CAS)
⭐ 轻量级锁 多线程但不冲突 小(自旋 CAS,不阻塞)
⭐ 重量级锁 多线程真冲突 大(OS 互斥、线程阻塞)
⭐ 升级路径:
  1. 对象头记下第一个访问它的线程 ID(偏向
  2. 第二个线程来抢 → 撤销偏向锁,升级为轻量级,两个线程 CAS 抢
  3. CAS 抢失败 / 自旋超阈值 → 升级为重量级,OS mutex,未抢到的线程挂起

⭐ 必背的 3 个注意点

  1. 锁的对象要一致synchronized (this)synchronized (obj) 是两把不同的锁
  2. 不要锁 String / Integer:它们在常量池可能被复用,导致"逻辑上不同的对象锁同一把锁"
  3. 锁内不要调用外部代码:外部代码可能慢 / 死锁 / 抛异常
5

volatile + JMM(Java 内存模型)⭐⭐

💡 动机:每个 Java 工程师都会背"synchronized 四大特性",但不知道为啥

JMM 三大问题

问题原因volatilesynchronized
⭐ 可见性 CPU 缓存:线程 A 改了,线程 B 看不见 ✅ 解决 ✅ 解决
⭐ 原子性 多步操作被切走(如 i++) ❌ 不解决 ✅ 解决
⭐ 有序性 JIT 重排序:编译器/CPU 优化打乱顺序 ✅ 部分解决 ✅ 解决
⭐ 经典口诀(synchronized 三大特性):

原子 + 可见 + 有序,一个锁解决三个问题,但代价是"性能"。
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();   // new 实际分 3 步:
                                                    //   1. 分配内存
                                                    //   2. 初始化对象
                                                    //   3. instance 指向内存
                                                    // volatile 防止 1→3→2 的重排
                }
            }
        }
        return instance;
    }
}
6

线程池 ThreadPoolExecutor ⭐⭐

💡 动机:生产代码 99% 用线程池,绝不能直接 new Thread

线程池的好处:复用线程控制并发数任务排队、统一异常处理。

7 大参数(必背)

new ThreadPoolExecutor(
    int corePoolSize,        // 核心线程数(即使空闲也不回收)
    int maximumPoolSize,     // 最大线程数
    long keepAliveTime,      // 空闲线程存活时间
    TimeUnit unit,           // 时间单位
    BlockingQueue<Runnable> workQueue,  // 任务队列
    ThreadFactory threadFactory,           // 线程创建工厂
    RejectedExecutionHandler handler        // 拒绝策略
);

⭐ 任务执行流程

// 提交任务 → 流程:
// 1. 线程数 < corePoolSize → 创建新线程执行
// 2. 线程数 ≥ corePoolSize → 任务进 workQueue
// 3. 队列满了 → 创建新线程(直到 maximumPoolSize)
// 4. 线程数 ≥ maximumPoolSize → 触发拒绝策略

⭐ 4 种拒绝策略

策略行为
AbortPolicy⭐ 默认,抛 RejectedExecutionException
CallerRunsPolicy⭐⭐ 由调用者线程执行(最推荐)
DiscardPolicy直接丢弃(不抛异常)
DiscardOldestPolicy丢最老的任务,再尝试提交
⭐ 生产推荐配置:
// CPU 密集型(计算)
int threads = Runtime.getRuntime().availableProcessors() + 1;

// I/O 密集型(数据库、网络)
int threads = Runtime.getRuntime().availableProcessors() * 2;

// 推荐:Executors.newFixedThreadPool(N) 固定大小
// 或 ThreadPoolExecutor 自定义(更可控)
7

JUC 工具类 + 常见面试题

💡 动机:JDK 5+ 加了 java.util.concurrent 包,并发开发已经不用裸 synchronized 了
工具类作用典型场景
ConcurrentHashMap ⭐⭐ 线程安全的 Map(分段锁 / CAS) 替代 HashtableCollections.synchronizedMap
CountDownLatch ⭐ 等 N 个线程都完成 主线程等所有子线程跑完
CyclicBarrier ⭐ N 个线程互相等,到齐一起跑 多线程分阶段计算
Semaphore ⭐ 控制同时只有 N 个线程进 限流
ThreadLocal ⭐ 每个线程自己的变量副本 事务、用户上下文
AtomicInteger ⭐ CAS 实现的原子计数器 替代 volatile int + synchronized
⭐ 经典面试题(背下来):
  1. synchronizedReentrantLock 区别?→ ReentrantLock 更灵活(可中断 / 公平锁 / tryLock),synchronized 是关键字、自动释放
  2. ConcurrentHashMap 怎么保证线程安全?→ JDK 7 分段锁,JDK 8+ 改成 CAS + synchronized 单桶锁
  3. ThreadLocal 内存泄漏怎么办?→ 用完 必须 remove(),特别是线程池场景
  4. waitsleep 区别?→ wait 释放锁,sleep 不释放;wait 必须在 synchronized 里
  5. 为什么 wait 必须在 synchronized 块里?→ 防止"伪唤醒"导致的竞态条件(著名的"lost wakeup"问题)

你的作业(分步走)

  1. 画线程状态图:凭记忆画"NEW → RUNNABLE → BLOCKED → TERMINATED"的状态转换图,标注每个状态会因什么方法触发
  2. 3 种创建方式:分别用 Thread / Runnable / Callable 写 3 个版本,启动 3 个线程,Callable 那个要拿返回值
  3. 复现竞态条件:写个 100 个线程同时 count++ 的程序(不用锁),看最终 count 不是 100;加锁后再跑对比
  4. volatile 验证可见性:写代码用 volatile 修饰 boolean flag,不开 volatile 时子线程可能死循环,开了之后能正常停止
  5. 线程池配置:手写 ThreadPoolExecutor不用 Executors),配 core=2, max=5, queue=10, 拒绝策略 CallerRunsPolicy,跑 20 个任务看输出
  6. 面试题答题:凭记忆答 3 道:① volatile 能替代锁吗 ② wait 和 sleep 区别 ③ ConcurrentHashMap JDK 8 怎么保证线程安全
  7. CountDownLatch 实战:主线程启动 5 个子线程做"并行计算",用 CountDownLatch 等全部完成再汇总结果