目录
正在加载目录…
专栏文章
专栏文章
Java 专栏
1. Java:跨平台语言与生态全景 2. Java 并发:JMM、锁与线程池原理 3. JVM:类加载、内存、GC 与性能诊断 4. Java 性能调优:CPU、内存、锁与 IO 排查 5. Java 新特性速查:从 Lambda 到虚拟线程 6. Java 业务开发:高频陷阱与 Code Review 清单 7. Java 核心知识:集合、并发与 JVM 面试要点 8. AQS 原理:同步队列与加锁解锁全流程 9. JVM 与 Linux 内存:分区、分配与 GC 边界 10. AtomicLong 与 LongAdder:并发计数器选型 11. Java 引用与内存泄漏:六类场景与修复 12. Java 测试实践:JUnit、Mockito 与 Testcontainers 13. Java 并发入门:线程、锁与线程池全景

Java 并发:JMM、锁与线程池原理

发布于 2026-06-17 14:40 · 最后编辑于 2026-07-31 15:51 · 字数 5,081 👁 275 次阅读

本文从并发 Bug 的三大根源出发,逐层深入分析 Java 内存模型、锁机制、AQS、线程池、并发容器与 ThreadLocal 的核心原理,结合 JDK 源码给出生产实践建议。

目录

章节说明
并发 Bug 的三大根源可见性、原子性、有序性
Java 内存模型(JMM)Happens-Before 规则、volatile、final
synchronized 原理偏向锁 → 轻量级锁 → 重量级锁
Lock 与 AQSReentrantLock、ReadWriteLock、StampedLock
AQS 内部实现CLH 队列、state 字段、acquire/release 源码、Condition
线程池ThreadPoolExecutor 参数详解、最佳实践
并发容器ConcurrentHashMap、CopyOnWriteArrayList、阻塞队列
原子类与 CASAtomicLong、LongAdder、ABA 问题
ThreadLocal实现原理、内存泄漏防范
并发设计模式不可变、COW、生产者-消费者
常见问题排查死锁、活锁、饥饿

并发 Bug 的三大根源

并发程序的诡异 Bug,根源无外乎三个:缓存导致的可见性问题线程切换带来的原子性问题编译优化带来的有序性问题

可见性:CPU 缓存的代价

多核 CPU 每个核心有自己的缓存,线程 A 在 CPU-1 上修改了变量,线程 B 在 CPU-2 上读取,可能读到旧值。

// 典型案例:两个线程各自累加 10000 次,结果不是 20000
public class VisibilityDemo {
    private long count = 0;

    private void add10K() {
        int idx = 0;
        while (idx++ < 10000) {
            count += 1;  // 非原子操作,且存在可见性问题
        }
    }
}

原子性:线程切换的代价

count += 1 在 CPU 层面是三条指令:读内存 → 加 1 → 写内存。操作系统可以在任意指令之间发生线程切换,导致两个线程同时基于旧值计算,写入结果互相覆盖。

核心认知:CPU 保证原子性的粒度是 CPU 指令,而非高级语言的一条语句。

有序性:编译优化的代价

编译器和处理器会对指令重排序以提升性能。经典案例是双重检查单例(DCL):

// 有问题的 DCL 单例——instance 未加 volatile
public class Singleton {
    static Singleton instance;

    static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null)
                    instance = new Singleton();  // 可能被重排序!
            }
        }
        return instance;
    }
}

new Singleton() 的正常步骤:①分配内存 → ②初始化对象 → ③将地址赋给 instance。 重排序后可能变为:①分配内存 → ②将地址赋给 instance → ③初始化对象。 若线程 B 在步骤 ② 之后读到 instance 不为 null,就会拿到一个未初始化的对象。

正确写法:给 instancevolatile

Java 内存模型(JMM)

JMM 的本质是:规范 JVM 如何提供按需禁用缓存和编译优化的方法,具体手段是 volatilesynchronizedfinal 三个关键字,以及六条 Happens-Before 规则。

Happens-Before 规则

Happens-Before 的含义不是"时间上先于",而是"前者的结果对后者可见"。

规则说明
程序顺序规则同一线程内,前面的操作 HB 后面的操作
volatile 变量规则volatile 写 HB 后续的 volatile 读
传递性规则A HB B,B HB C,则 A HB C
管程中锁的规则解锁 HB 后续对同一锁的加锁
线程 start() 规则主线程 start() HB 子线程的所有操作
线程 join() 规则子线程所有操作 HB 主线程 join() 返回

volatile 的两层语义

  1. 禁用 CPU 缓存:读写必须直接操作内存(不使用寄存器缓存)
  2. 禁止指令重排序(Java 1.5 增强):volatile 写操作之前的所有操作,对 volatile 读之后的所有操作可见
class VolatileExample {
    int x = 0;
    volatile boolean v = false;

    public void writer() {
        x = 42;    // happens-before v=true(程序顺序规则)
        v = true;  // volatile 写
    }

    public void reader() {
        if (v == true) {
            // x 一定是 42(volatile 规则 + 传递性)
            System.out.println(x);
        }
    }
}

final 的内存语义

final 字段在构造函数中完成初始化后,其他线程可以看到正确的值。但要避免"逸出"——在构造函数中将 this 赋给全局变量:

// 错误:this 逸出,其他线程可能看到未初始化的 final 字段
public class FinalFieldExample {
    final int x;
    static FinalFieldExample instance;

    public FinalFieldExample() {
        x = 3;
        instance = this;  // 逸出!x 可能尚未初始化
    }
}

synchronized 原理

../../assets/02 Java 并发编程/file 20260605123642193

字节码层面

synchronized 代码块编译后包含 monitorentermonitorexit 指令;synchronized 方法则在访问标记中设置 ACC_SYNCHRONIZED

每个锁对象维护锁计数器持有线程指针,支持可重入(同一线程多次加锁,计数器递增)。

锁升级路径

HotSpot 的锁实现分三层,按竞争程度自动升级(不可降级):

flowchart LR
    A["无锁<br/>01"] -->|首次加锁| B["偏向锁<br/>101"]
    B -->|其他线程竞争| C["轻量级锁<br/>00"]
    C -->|竞争激烈| D["重量级锁<br/>10"]
    style B fill:#cfc,stroke:#060
    style C fill:#ff9,stroke:#960
    style D fill:#fcc,stroke:#c00
锁类型适用场景实现机制代价
偏向锁只有一个线程反复获取CAS 将线程地址写入对象头 mark word极低(第一次 CAS,后续直接返回)
轻量级锁多线程在不同时间段获取CAS 将 mark word 替换为锁记录指针低(自旋等待)
重量级锁多线程同时竞争操作系统 mutex,阻塞/唤醒线程高(系统调用,用户态→内核态)

自适应自旋:重量级锁在阻塞前会先自旋,根据历史成功率动态调整自旋次数。

正确使用 synchronized

// 保护静态变量:锁 Class 对象
class SafeCounter {
    static long value = 0L;

    // 两个方法必须用同一把锁,否则可见性无法保证
    synchronized static long get() {
        return value;
    }

    synchronized static void addOne() {
        value += 1;
    }
}

⚠️ 常见错误:用 synchronized(new Object()) 每次都创建新锁对象,完全无效。

Lock 与 AQS

../../assets/02 Java 并发编程/file 20260605123641999

为什么需要 Lock

synchronized 无法解决"不可抢占"问题:线程申请锁失败后直接阻塞,无法主动释放已持有的锁。Lock 提供了三种解决方案:

// 1. 支持中断(破坏不可抢占)
void lockInterruptibly() throws InterruptedException;

// 2. 支持超时
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

// 3. 非阻塞获取
boolean tryLock();

ReentrantLock 使用规范

import java.util.concurrent.locks.ReentrantLock;

class BankAccount {
    private final ReentrantLock lock = new ReentrantLock();
    private int balance;

    public void transfer(BankAccount target, int amount) {
        lock.lock();
        try {
            target.lock.lock();
            try {
                this.balance -= amount;
                target.balance += amount;
            } finally {
                target.lock.unlock();  // finally 中释放,防止死锁
            }
        } finally {
            lock.unlock();
        }
    }
}

ReentrantLock 可见性保证

ReentrantLock 内部有一个 volatile int state 字段。加锁时读写 state,解锁时也读写 state。根据 volatile 的 Happens-Before 规则,解锁操作 HB 后续加锁操作,从而保证可见性。

公平锁 vs 非公平锁

// 非公平锁(默认):性能更好,但可能饥饿
ReentrantLock unfairLock = new ReentrantLock();

// 公平锁:按等待时间顺序唤醒,吞吐量较低
ReentrantLock fairLock = new ReentrantLock(true);

ReadWriteLock:读多写少场景

import java.util.concurrent.locks.ReentrantReadWriteLock;

class Cache {
    private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    private final Map<String, Object> map = new HashMap<>();

    public Object get(String key) {
        rwl.readLock().lock();
        try {
            return map.get(key);
        } finally {
            rwl.readLock().unlock();
        }
    }

    public void put(String key, Object value) {
        rwl.writeLock().lock();
        try {
            map.put(key, value);
        } finally {
            rwl.writeLock().unlock();
        }
    }
}

StampedLock:乐观读

StampedLockReadWriteLock 基础上增加了乐观读,读操作不加锁,只在最后校验是否有写操作发生:

import java.util.concurrent.locks.StampedLock;

class Point {
    private final StampedLock sl = new StampedLock();
    private double x, y;

    public double distanceFromOrigin() {
        // 乐观读:不加锁
        long stamp = sl.tryOptimisticRead();
        double curX = x, curY = y;

        // 校验期间是否有写操作
        if (!sl.validate(stamp)) {
            // 升级为悲观读锁
            stamp = sl.readLock();
            try {
                curX = x;
                curY = y;
            } finally {
                sl.unlockRead(stamp);
            }
        }
        return Math.sqrt(curX * curX + curY * curY);
    }
}

⚠️ StampedLock 不支持重入,且不支持条件变量(Condition)。

线程池

../../assets/02 Java 并发编程/file 20260605123642422

为什么用线程池

创建线程需要调用操作系统内核 API,分配大量资源,线程是重量级对象,频繁创建销毁代价极高。线程池采用生产者-消费者模式:调用方(生产者)提交任务到队列,池内线程(消费者)循环消费。

ThreadPoolExecutor 七个参数

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    4,                                  // corePoolSize:核心线程数,常驻
    8,                                  // maximumPoolSize:最大线程数
    60L,                                // keepAliveTime:空闲线程存活时间
    TimeUnit.SECONDS,                   // unit
    new ArrayBlockingQueue<>(100),      // workQueue:有界队列(强烈推荐)
    new ThreadFactory() {               // threadFactory:自定义线程名
        private final AtomicInteger n = new AtomicInteger(0);
        public Thread newThread(Runnable r) {
            return new Thread(r, "biz-pool-" + n.getAndIncrement());
        }
    },
    new ThreadPoolExecutor.CallerRunsPolicy()  // handler:拒绝策略
);
参数说明
corePoolSize常驻线程数,即便空闲也不回收
maximumPoolSize队列满后可临时扩展到的最大线程数
keepAliveTime超出 core 的线程空闲超时后回收
workQueue任务缓冲队列,必须有界(防 OOM)
threadFactory自定义线程名,便于排查问题
handler队列满且线程数达上限时的拒绝策略

四种拒绝策略

策略行为适用场景
AbortPolicy(默认)抛出 RejectedExecutionException需要感知过载
CallerRunsPolicy由提交线程自己执行降级处理,不丢任务
DiscardPolicy直接丢弃,不抛异常可接受丢失
DiscardOldestPolicy丢弃队列最老的任务优先保证新任务

线程数设置原则

CPU 密集型:线程数 ≈ CPU 核心数 + 1
IO 密集型:线程数 ≈ CPU 核心数 × (1 + IO 等待时间 / CPU 计算时间)

最佳实践

// 1. 异常处理:execute() 提交的任务异常会被吞掉
executor.execute(() -> {
    try {
        // 业务逻辑
    } catch (RuntimeException e) {
        // 显式捕获处理
        log.error("task failed", e);
    }
});

// 2. 优雅关闭
executor.shutdown();  // 不再接受新任务,等待已提交任务完成
// 或
executor.shutdownNow();  // 中断所有任务

⚠️ 禁止使用 Executors.newFixedThreadPool():默认使用无界 LinkedBlockingQueue,高负载下极易 OOM。

并发容器

同步容器 vs 并发容器

Java 1.5 之前的同步容器(VectorHashtableCollections.synchronizedXxx())基于 synchronized 全方法加锁,串行度高,性能差。Java 1.5+ 提供了高性能并发容器。

ConcurrentHashMap

  • Java 7:分段锁(Segment),每个 Segment 是一个小 HashMap,并发度 = Segment 数量(默认 16)
  • Java 8:CAS + synchronized,锁粒度降至单个桶(数组槽),并发度大幅提升
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key", 1);
// 原子性的复合操作
map.putIfAbsent("key", 2);       // 不存在才插入
map.computeIfAbsent("key", k -> k.length());  // 不存在则计算并插入

⚠️ ConcurrentHashMap 的 key 和 value 都不能为 null(HashMap 允许)。

CopyOnWriteArrayList

写时复制:写操作在新副本上进行,完成后将引用指向新数组。读操作完全无锁。

CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("a");  // 创建新数组副本,写入后替换
特性说明
适用场景读多写极少(如配置列表、监听器列表)
注意事项写操作代价高;读到的数据可能不是最新(快照语义)
迭代器只读,不支持 remove()

阻塞队列

类型实现特点
单端阻塞ArrayBlockingQueue有界,数组实现,公平/非公平
单端阻塞LinkedBlockingQueue可有界可无界,链表实现
单端阻塞SynchronousQueue无缓冲,生产者必须等消费者
单端阻塞PriorityBlockingQueue按优先级出队,无界
单端阻塞DelayQueue延时出队,无界
双端阻塞LinkedBlockingDeque双端,链表实现
单端非阻塞ConcurrentLinkedQueue无界,CAS 实现

⚠️ 线程池的 workQueue 必须用有界队列,避免 OOM。

并发容器选型

需求推荐
线程安全 MapConcurrentHashMap
有序 key 的线程安全 MapConcurrentSkipListMap(跳表,O(log n))
读多写极少的 ListCopyOnWriteArrayList
生产者-消费者队列ArrayBlockingQueue(有界)或 LinkedBlockingQueue(有界)

原子类与 CAS

CAS 原理

CAS(Compare And Swap)是一条 CPU 原子指令:只有当内存中的值等于期望值时,才将其更新为新值。

// CAS 的伪代码
synchronized int cas(int expect, int newValue) {
    int cur = count;
    if (cur == expect) {
        count = newValue;
    }
    return cur;  // 返回写入前的值
}

// CAS + 自旋实现无锁累加
void addOne() {
    int newVal;
    do {
        newVal = count + 1;
    } while (count != cas(count, newVal));
}

原子类体系

// 1. 基本类型
AtomicInteger count = new AtomicInteger(0);
count.getAndIncrement();   // i++
count.incrementAndGet();   // ++i
count.compareAndSet(1, 2); // CAS

// 2. 对象引用(处理 ABA 问题用 AtomicStampedReference)
AtomicReference<String> ref = new AtomicReference<>("v1");
AtomicStampedReference<String> stampedRef =
    new AtomicStampedReference<>("v1", 0);

// 3. 高并发累加器(比 AtomicLong 更快)
LongAdder adder = new LongAdder();
adder.increment();
adder.sum();

ABA 问题

线程 T1 读到值 A,T2 将 A 改为 B,T3 再改回 A,T1 的 CAS 成功但实际上值已经变过——这就是 ABA 问题。

解决方案:AtomicStampedReference 附加版本号:

AtomicStampedReference<Integer> ref =
    new AtomicStampedReference<>(100, 0);

int[] stampHolder = new int[1];
int val = ref.get(stampHolder);
int stamp = stampHolder[0];

// 必须 value 和 stamp 都匹配才能更新
ref.compareAndSet(val, val + 1, stamp, stamp + 1);

原子类 vs 互斥锁

维度原子类(CAS)互斥锁
性能高(无阻塞,无上下文切换)低(系统调用)
适用场景单个变量的简单操作多个变量的复合操作
死锁风险无(但可能活锁/饥饿)

ThreadLocal

核心用途

为每个线程提供独立的变量副本,彻底消除共享,从根本上避免并发问题。

// 典型场景:线程安全的 SimpleDateFormat
class SafeDateFormat {
    static final ThreadLocal<DateFormat> tl = ThreadLocal.withInitial(
        () -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
    );

    static DateFormat get() {
        return tl.get();
    }
}

实现原理

关键设计:ThreadLocalMap 属于 Thread,而非 ThreadLocal。

Thread
  └── threadLocals: ThreadLocalMap
        └── Entry[](数组,Key 是 ThreadLocal 的弱引用,Value 是变量值)
  • 每个线程持有自己的 ThreadLocalMap
  • ThreadLocal.get() 实际是:Thread.currentThread().threadLocals.getEntry(this)
  • Key 是 WeakReference<ThreadLocal>,ThreadLocal 对象可被 GC 回收
  • Value 是强引用,这是内存泄漏的根源

内存泄漏防范

在线程池中使用 ThreadLocal 极易内存泄漏:线程池的线程长期存活,ThreadLocalMap 不会被回收,Value 持续占用内存。

ExecutorService executor = Executors.newFixedThreadPool(4);
ThreadLocal<Object> tl = new ThreadLocal<>();

executor.execute(() -> {
    tl.set(new LargeObject());
    try {
        // 业务逻辑
    } finally {
        tl.remove();  // 必须手动清理!
    }
});

⚠️ 在线程池中使用 ThreadLocal,必须在 finally 块中调用 tl.remove()

InheritableThreadLocal

子线程可以继承父线程的 ThreadLocal 值,但不建议在线程池中使用:线程池中的线程是复用的,继承关系可能错乱,导致业务逻辑错误。

并发设计模式

不可变模式(Immutability)

不可变对象天然线程安全,无需任何同步。

// 使用 final 字段 + 不提供修改方法
public final class ImmutablePoint {
    private final double x;
    private final double y;

    public ImmutablePoint(double x, double y) {
        this.x = x;
        this.y = y;
    }

    // 修改操作返回新对象,不修改原对象
    public ImmutablePoint translate(double dx, double dy) {
        return new ImmutablePoint(x + dx, y + dy);
    }
}

Copy-on-Write 模式

写时复制:写操作先复制一份,在副本上修改,完成后原子替换引用。适合读多写少场景。

// 自定义 COW 容器示例
class CopyOnWriteList<T> {
    private volatile List<T> list = new ArrayList<>();

    public synchronized void add(T e) {
        List<T> newList = new ArrayList<>(list);
        newList.add(e);
        list = newList;  // 原子替换引用
    }

    public List<T> get() {
        return list;  // 读操作无锁
    }
}

生产者-消费者模式

解耦生产和消费速度,通过阻塞队列作为缓冲区:

BlockingQueue<Task> queue = new ArrayBlockingQueue<>(100);

// 生产者
Thread producer = new Thread(() -> {
    while (true) {
        queue.put(generateTask());  // 队列满时阻塞
    }
});

// 消费者
Thread consumer = new Thread(() -> {
    while (true) {
        Task task = queue.take();  // 队列空时阻塞
        process(task);
    }
});

常见问题排查

死锁

条件:互斥、占有且等待、不可抢占、循环等待(四个条件同时成立)。

检测jstack <pid> 会自动识别并打印死锁信息。

预防

  • 按固定顺序获取锁(破坏循环等待)
  • 使用 tryLock() 超时机制(破坏不可抢占)
  • 减少锁的粒度
// 转账场景:按账户 ID 顺序加锁,避免死锁
void transfer(Account from, Account to, int amount) {
    Account first = from.id < to.id ? from : to;
    Account second = from.id < to.id ? to : from;
    synchronized (first) {
        synchronized (second) {
            from.balance -= amount;
            to.balance += amount;
        }
    }
}

活锁与饥饿

问题现象解决方案
死锁线程永久阻塞,等待永不发生的条件按序加锁、超时机制
活锁线程不断重试但都失败,CPU 飙高引入随机退避(exponential backoff)
饥饿某线程长期得不到 CPU 或锁使用公平锁,避免长时间持有锁

性能问题排查

现象可能原因排查方向
CPU 100%死循环、自旋过多jstack 查看线程状态,找 RUNNABLE 线程
线程大量 BLOCKED锁竞争激烈jstack 查看锁持有情况,减小锁粒度
OOM无界队列堆积、ThreadLocal 泄漏jmap -histo 查看大对象
吞吐量低线程数不足或过多调整线程池参数,监控队列积压

参考资料

← 返回列表

评论 (0)

暂无评论,来留下第一条吧。
登录注册 后才能发表评论