Skip to content

多线程与并发基础 ​

Java 中级 · Java 进阶

并发是 Java 最硬的一块骨头。本篇不满足于「会写 synchronized」,而是讲清楚:多线程到底共享了什么、锁是怎么升级的、volatile 靠什么保证可见性、线程池为什么不能乱用 Executors。这些是高级篇 JUC 源码的地基。

一、线程的本质与共享内存 ​

1.1 原生线程模型 ​

Java 线程与操作系统线程一一对应(Thread.start() 底层调用 pthread_create),调度权在 OS。所以「几百个线程」就会带来上下文切换开销。

java
Thread t = new Thread(() -> System.out.println("run in " + Thread.currentThread().getName()));
t.start();   // 启动新线程,进入就绪态
t.join();    // 主线程阻塞等待 t 结束
状态说明
NEW已创建未 start
RUNNABLE可运行(含 OS 就绪+运行)
BLOCKED等待进入 synchronized
WAITINGwait() / join() / park() 无限等待
TIMED_WAITINGsleep(n) / wait(n)
TERMINATED结束

1.2 共享内存带来的三个问题 ​

java
// 经典可见性 bug:程序可能永不停止
static boolean flag = false;
new Thread(() -> { while (!flag) {} }).start();
Thread.sleep(100); flag = true;   // 主线程改了,但子线程可能一直读到旧值(CPU 缓存)

原因:每个线程有自己的工作内存(CPU 缓存),不是直接读写主内存。volatile / synchronized 负责在必要时刷新缓存。

二、synchronized:从偏向锁到重量级锁 ​

synchronized 保证原子性、可见性、有序性。它的锁对象信息存在对象头的 Mark Word 里。

2.1 锁升级过程(JDK 15 前) ​

  • 偏向锁:只有一个线程访问时,把线程 ID 写进 Mark Word,之后进入无需 CAS(JDK 15 后已默认禁用/废弃偏向锁)。
  • 轻量级锁:多线程交替执行、无真正并发时,用 CAS 自旋抢占,避免线程挂起。
  • 重量级锁:竞争激烈时升级为 OS 互斥量,未抢到的线程挂起(用户态→内核态切换,开销大)。

2.2 用法与陷阱 ​

java
public class Counter {
    private int count = 0;
    public synchronized void incr() { count++; }          // 锁 this
    public static synchronized void staticIncr() {}        // 锁 Counter.class
    public void block() {
        synchronized (this) { count++; }                   // 锁代码块,粒度更小
    }
}

锁的对象别用错

  • 锁 this、锁 X.class、锁一个 private final Object lock 是三把不同的锁,混用等于没锁。
  • 别锁 String 常量或 Integer:常量池/缓存会复用对象,不同逻辑锁到同一个对象,互相阻塞。
  • 别锁可变引用:锁对象一旦被重新赋值,锁就换了。

三、volatile:可见性与有序性(但不保证原子性) ​

volatile 让变量的读写直接作用于主内存,并通过内存屏障禁止重排。

java
private volatile boolean running = true;

public void stop() { running = false; }        // 其它线程立刻能看到
public void run()  { while (running) { /* ... */ } }
保证volatilesynchronized
可见性✅✅
有序性✅(禁止重排)✅
原子性❌(i++ 仍不安全)✅
java
// volatile 不保证原子性的反例:并发下结果 < 2000
private volatile int count = 0;
void incr() { count++; }   // 读-改-写三步,volatile 挡不住交错
// 正解:AtomicInteger 或 synchronized

内存屏障(底层实现):

text
volatile 写前:StoreStore 屏障(前面的普通写先落主存)
volatile 写后:StoreLoad 屏障(把写刷回主存,触发其它 CPU 缓存失效)
volatile 读后:LoadLoad / LoadStore 屏障

四、JMM 与 happens-before ​

JMM(Java 内存模型)规定:只要满足 happens-before 规则,前一个操作的可见性与有序性就对后一个操作可见。

happens-before 规则含义
程序顺序同一线程内,前面代码 hb 后面代码
锁规则解锁 hb 后续对该锁的加锁
volatile写 hb 后续读
传递性A hb B,B hb C ⇒ A hb C
start()主线程 start 前的操作 hb 子线程
join()子线程所有操作 hb join 返回

五、线程池:别用 Executors ​

java
ThreadPoolExecutor pool = new ThreadPoolExecutor(
    4,                              // corePoolSize 核心线程数
    8,                              // maximumPoolSize 最大线程数
    60L, TimeUnit.SECONDS,          // 空闲回收时间(非核心线程)
    new LinkedBlockingQueue<>(200), // 任务队列(有界!)
    new ThreadFactory() {            // 自定义线程名,便于排查
        private final AtomicInteger i = new AtomicInteger(1);
        public Thread newThread(Runnable r) {
            return new Thread(r, "order-pool-" + i.getAndIncrement());
        }
    },
    new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:调用者执行,天然反压
);

5.1 执行流程 ​

5.2 四种拒绝策略 ​

策略行为
AbortPolicy(默认)抛 RejectedExecutionException
CallerRunsPolicy提交任务的线程自己执行(降速反压)
DiscardPolicy静默丢弃
DiscardOldestPolicy丢队列最老任务再提交

为什么禁止 Executors.newXxx()

  • newFixedThreadPool / newSingleThreadExecutor 用无界队列 LinkedBlockingQueue,任务堆积会 OOM。
  • newCachedThreadPool / newScheduledThreadPool 最大线程数是 Integer.MAX_VALUE,会无限创建线程耗尽资源。
  • 生产必须手动 new ThreadPoolExecutor,队列有界、线程命名、拒绝策略明确。

参数怎么定:

  • CPU 密集:核数 + 1
  • IO 密集:核数 × (1 + 平均等待时间/平均计算时间),通常 核数 × 2 起步,压测微调。

六、最佳实践 ​

  • 共享可变状态最小化:能用不可变对象、局部变量、ThreadLocal 就别共享。
  • 优先 java.util.concurrent 工具(AtomicInteger、ConcurrentHashMap、CountDownLatch),而不是裸 synchronized。
  • 线程池手动创建 + 监控队列长度/活跃数,接入告警。
  • ThreadLocal 用完必须 remove(),否则线程池复用会造成内存泄漏/串数据。
  • 避免死锁:多把锁按固定顺序获取,或用 tryLock(timeout)。

📚 官方文档与参考资料 ​

✏️ 练习与自测 ​

  1. 用 javap 或 jol 观察一个对象加锁前后 Mark Word 的变化,说明锁升级过程。
  2. 为什么 volatile int count 的 count++ 在并发下仍会丢结果?写出两种正确改法。
  3. 描述 ThreadPoolExecutor 提交任务时的完整决策流程,并说明为什么 newFixedThreadPool 会导致 OOM。
  4. 写一个会产生死锁的例子,再用「固定加锁顺序」或 tryLock 修复它。

本节小结

并发的三大问题——可见性、原子性、有序性,分别靠 volatile(可见+有序)、synchronized/原子类(原子)、内存屏障(有序)解决。synchronized 有偏向→轻量→重量的锁升级;volatile 不保证原子性。线程池手动创建、队列有界、线程命名、拒绝策略明确,禁用 Executors 工厂方法。