java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java并发编程核心知识点梳理

面试与实战必备:掌握Java并发编程关键知识点(线程池/CAS/性能优化)

作者:百锦再@新空间创想科技

本文从工程视角系统梳理Java并发编程的关键知识点,主要内容包括:并发问题的本质、Java内存模型、volatile和synchronized、Lock与AQS、线程池、Future与CompletableFuture、CAS与原子类、并发容器、死锁与线上排查、并发性能优化思路等一些常用代码片段


前言

Java 并发编程是后端开发中非常核心的一项能力。

很多线上问题表面上看起来是“偶发故障”,实际上根因都和并发有关,比如:

很多人学习并发时停留在 API 层,知道 synchronizedvolatileThreadPoolExecutor 的写法,但遇到线上问题时,仍然很难定位和判断。

这篇文章从工程视角出发,系统梳理 Java 并发编程的关键知识点,帮助你把并发能力真正转化成可落地的工程判断力。

本文主要内容包括:

一、为什么并发问题总是在生产环境暴露

并发问题有一个典型特点:

本地不容易复现,线上高峰期集中爆发。

原因很简单。

并发 bug 往往和时序有关,而时序又受到下面这些因素影响:

在本地开发环境下,请求量小、线程数少、资源竞争弱,很多问题根本暴露不出来。一旦进入高并发、长时间运行环境,隐藏问题就会被放大。

常见表现包括:

这类问题如果没有并发基础,很难真正定位。

二、并发编程到底在解决什么

并发编程的本质不是“让代码显得高级”,而是为了在有限资源下提高吞吐、提升响应效率,并确保结果正确。

它主要解决三类问题:

1. 性能问题

多个任务并行处理,提高资源利用率。

2. 正确性问题

多个线程同时读写共享数据时,如何保证结果不出错。

3. 可控性问题

线程数量、任务队列、锁竞争、资源消耗必须有边界。

很多开发者只盯着“并发更快”,但生产系统里更关键的是:

并发之后还能不能稳定、可控、可排查。

三、进程、线程、协程先区分清楚

1. 进程

进程是资源分配的基本单位。一个 Java 应用启动后,对应一个独立进程。

2. 线程

线程是 CPU 调度的基本单位。一个进程内部可以有多个线程,这些线程共享堆内存、方法区等资源。

3. 协程

协程是更轻量级的执行单元,通常在用户态调度。Java 传统并发主要以线程为核心,不过现在虚拟线程也在逐步发展。

对于大多数 Java 后端开发者来说,目前最需要掌握的仍然是线程模型,因为:

这些核心知识都围绕线程展开。

四、并发 bug 的根源:共享、竞争、时序

并发代码为什么难,根本原因就在于这三件事同时存在:

1. 共享

多个线程访问同一份资源。

2. 竞争

多个线程同时修改共享资源。

3. 时序不可预测

线程何时运行、何时切换、谁先谁后,开发者很难完全控制。

只要代码中出现“共享变量”,就要立刻问自己三个问题:

如果答案是“多个线程访问,且至少一个线程会写”,那就必须明确线程安全策略。

五、Java 内存模型为什么必须理解

并发问题很多时候不是代码逻辑错,而是线程看到的变量值不一致。

Java 内存模型,也就是 JMM,规定了:

理解 JMM,至少要掌握三个概念:

1. 原子性

一个操作是否不可分割。

2. 可见性

一个线程修改变量后,另一个线程能否立刻看到。

3. 有序性

程序执行顺序是否和代码书写顺序一致。

举个例子:

count++;

这不是原子操作,它至少包括三步:

因此在多线程下会出现数据竞争。

六、volatile 的作用与边界

volatile 的核心能力是:

示例:

private volatile boolean running = true;

一个线程将 running 改成 false,另一个线程通常可以很快读到最新值。

volatile 适合什么场景

volatile 不适合什么场景

例如下面代码依然线程不安全:

private volatile int count = 0;

public void increment() {
    count++;
}

因为 count++ 不是原子操作。

所以一定要记住一句话:

volatile 保证可见性,不保证复合操作原子性。

七、synchronized 的本质

synchronized 是 Java 最基础的内置锁。

它能保证:

示例:

public synchronized void add() {
    count++;
}

或者:

synchronized (lock) {
    count++;
}

synchronized 适合什么场景

很多开发者印象里觉得 synchronized 性能很差,这个认知已经过时。JDK 对它做过大量优化,很多场景下完全够用。

八、ReentrantLock 为什么存在

如果你对锁有更细粒度的控制需求,ReentrantLock 就比 synchronized 更适合。

示例:

private final ReentrantLock lock = new ReentrantLock();

public void add() {
    lock.lock();
    try {
        count++;
    } finally {
        lock.unlock();
    }
}

ReentrantLock 的优势

什么时候用 ReentrantLock

但同时要注意:

显式锁一定要在 finally 中释放。

九、AQS 到底是什么

AQS,全称 AbstractQueuedSynchronizer,是 Java 并发包里非常核心的同步框架。

很多常见同步工具都建立在 AQS 之上,例如:

AQS 的核心思路可以概括为:

理解 AQS 的意义不在于每天自己手写同步器,而在于你能真正理解:

十、synchronized 和 ReentrantLock 怎么选

简单来说可以这样判断:

优先用 synchronized 的场景

优先用 ReentrantLock 的场景

不要为了“显得专业”把所有同步都替换成 ReentrantLock。大多数场景,简单的方案更稳。

十一、读写锁什么时候适合使用

如果一个共享资源是典型的“读多写少”,读写锁通常能带来更好的并发能力。

示例:

private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();

读操作:

readLock.lock();
try {
    return cache.get(key);
} finally {
    readLock.unlock();
}

写操作:

writeLock.lock();
try {
    cache.put(key, value);
} finally {
    writeLock.unlock();
}

适合读写锁的场景

不适合的场景

十二、线程池为什么是并发系统的核心

生产环境里,几乎不应该频繁手动 new Thread()

原因很简单:

线程池的核心价值就是:

统一管理线程资源,并建立明确边界。

线程池的关键参数包括:

示例:

ExecutorService executor = new ThreadPoolExecutor(
        8,
        16,
        60L,
        TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(1000),
        Executors.defaultThreadFactory(),
        new ThreadPoolExecutor.CallerRunsPolicy()
);

十三、为什么生产环境不建议直接用 Executors

很多人习惯直接这样写:

Executors.newFixedThreadPool(10);

或者:

Executors.newCachedThreadPool();

这类 API 虽然方便,但在生产环境通常不推荐直接使用,原因是关键参数被隐藏了。

典型风险

1. newFixedThreadPool

默认使用无界队列,任务堆积时可能不断占用内存。

2. newCachedThreadPool

线程数增长过快时可能导致大量线程创建,造成调度和资源压力。

生产环境更合理的方式是:

显式使用 ThreadPoolExecutor,把线程数、队列长度、拒绝策略写清楚。

十四、线程池参数如何估算

线程池没有万能配置,但可以从任务类型入手。

1. CPU 密集型任务

例如:

这类任务线程数通常接近 CPU 核数。

经验值:

CPU核数 + 1

2. IO 密集型任务

例如:

这类任务线程经常在等待,可以适当提高线程数。

3. 真正的调优原则

线程池参数不能只靠经验公式,最终一定要看:

十五、拒绝策略为什么不能忽略

线程池不是无限吞任务的。

当线程数达到上限、队列也满了之后,线程池会触发拒绝策略。

JDK 常见拒绝策略有四种:

各自特点

1. AbortPolicy

直接抛异常。适合需要明确感知失败的场景。

2. CallerRunsPolicy

由提交任务的线程自己执行,适合做一定程度的反压。

3. DiscardPolicy

直接丢弃任务,不抛异常。高风险,不适合关键任务。

4. DiscardOldestPolicy

丢弃最早排队的任务。是否合理取决于业务语义。

最危险的不是哪种策略不好,而是:

系统已经在拒绝任务了,但没人知道。

所以拒绝次数一定要纳入监控。

十六、Future、Callable、CompletableFuture 的使用场景

1. Callable 和 Future

Callable 相比 Runnable,支持返回值和异常。

示例:

Future<Integer> future = executor.submit(() -> 1 + 2);
Integer result = future.get();

问题在于:

2. CompletableFuture

如果需要做异步编排,CompletableFuture 更适合。

例如:

CompletableFuture<User> userFuture =
        CompletableFuture.supplyAsync(() -> userService.getUser(userId), executor);

CompletableFuture<List<Order>> orderFuture =
        CompletableFuture.supplyAsync(() -> orderService.getOrders(userId), executor);

CompletableFuture<UserDetailDTO> resultFuture =
        userFuture.thenCombine(orderFuture, UserDetailDTO::new);

使用 CompletableFuture 的注意点

十七、CAS 和原子类的意义

CAS 是 Compare-And-Swap,通过比较当前值与期望值是否一致来决定是否更新。

Java 中常见原子类包括:

示例:

AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();

CAS 的优点

CAS 的限制

所以原子类适合做:

不适合直接替代所有加锁场景。

十八、AtomicLong 和 LongAdder 怎么选

AtomicLong 适合

LongAdder 适合

原因是 LongAdder 会分散竞争热点,最后再汇总结果,在高并发统计场景下通常更有优势。

十九、并发容器如何选型

常见并发容器包括:

1. ConcurrentHashMap

适合缓存、本地索引、状态映射。

ConcurrentHashMap<String, Object> cache = new ConcurrentHashMap<>();

2. CopyOnWriteArrayList

适合读多写少场景,比如监听器列表。

3. BlockingQueue

适合生产者消费者模型、异步任务缓冲。

4. ConcurrentLinkedQueue

适合高并发无阻塞队列场景。

这里一定要记住一句话:

并发容器保证的是单次容器操作线程安全,不保证多个组合操作天然线程安全。

例如下面写法就不是绝对安全的:

if (!map.containsKey(key)) {
    map.put(key, value);
}

应优先使用:

map.putIfAbsent(key, value);

或者:

map.computeIfAbsent(key, k -> createValue(k));

二十、阻塞队列的重要性

阻塞队列在并发系统里非常关键,尤其在线程池和生产者消费者模型中。

常见阻塞队列有:

使用阻塞队列时要考虑什么

这里最重要的一个原则是:

高并发系统优先考虑有界队列。

无界队列会把问题“延后爆炸”,而不是解决问题。

二十一、死锁为什么难排查

死锁通常发生在多个线程相互等待对方释放资源时。

典型场景:

示例:

synchronized (lockA) {
    synchronized (lockB) {
    }
}

另一段代码反过来:

synchronized (lockB) {
    synchronized (lockA) {
    }
}

预防死锁的原则

线上排查死锁最常用的手段是:

jstack

二十二、ThreadLocal 为什么常用又危险

ThreadLocal 用来为每个线程保存独立变量副本。

适合场景:

示例:

private static final ThreadLocal<String> CURRENT_USER = new ThreadLocal<>();

风险点

如果在线程池环境中使用 ThreadLocal,但没有及时清理,线程复用后就可能读到旧值,甚至导致内存泄漏。

正确写法:

try {
    CURRENT_USER.set(userId);
    // 业务逻辑
} finally {
    CURRENT_USER.remove();
}

结论很明确:

线程池环境中使用 ThreadLocal,一定要 remove。

二十三、并发性能优化中的常见误区

误区 1:线程越多越快

错误。线程太多会带来上下文切换和资源竞争。

误区 2:锁一定很慢

错误。低竞争场景下锁可能非常高效。

误区 3:异步一定比同步高级

错误。异步只是改变执行方式,不一定更优。

误区 4:CPU 高说明系统在努力工作

错误。高 CPU 可能是自旋、死循环、过度序列化、GC 或竞争。

误区 5:用了线程池就万事大吉

错误。线程池参数配置、队列长度、拒绝策略、任务拆分方式都会决定最终效果。

并发优化必须建立在真实监控和压测数据之上,而不是凭感觉。

二十四、线上并发问题如何排查

排查并发问题时,建议按下面顺序看。

1. 先看症状

2. 看线程池

3. 看线程栈

使用 jstack 查看:

4. 看下游资源

5. 看 GC

长时间 STW 也会放大并发问题表象。

6. 看业务设计

二十五、一个典型线程池事故案例

假设一个订单系统,一个请求会触发这些动作:

开发者为了“提升性能”,把后面所有动作都异步化,丢进同一个线程池。

线程池参数如下:

低峰期没问题,高峰期库存服务一慢,异步任务消费速度下降。因为队列无界,任务会不断积压。短时间内看不出异常,但随着任务越堆越多,内存、GC、延迟都会变差,最后系统整体雪崩。

这个案例说明几个关键问题:

二十六、并发代码设计的硬规则

下面这些原则非常实用。

1. 优先减少共享状态

不共享,就没有竞争。

2. 显式创建线程池

不要依赖默认线程池或偷懒工厂方法。

3. 所有共享变量都要明确线程安全策略

要么锁、要么原子类、要么线程封闭、要么不可变。

4. 组合操作要特别谨慎

容器线程安全不代表业务逻辑整体线程安全。

5. 锁粒度尽量小

持锁期间不要做慢操作、远程调用、数据库操作。

6. 高并发热点计数优先用 LongAdder

不要让所有线程争抢同一个热点原子变量。

7. 线程池、锁、队列必须有监控

不可观测的并发系统基本不可维护。

8. 不要为了并发而并发

如果真正瓶颈在数据库或网络,多开线程不一定有效。

二十七、如何高效学习 Java 并发

学习并发最有效的方式不是死记硬背,而是结合代码和问题去理解。

建议从三个方向入手。

1. 自己写小实验

例如:

2. 结合源码学习

重点建议看:

3. 带着线上问题学习

比如:

有问题驱动,理解会更扎实。

总结

Java 并发编程不是几个 API 的使用技巧,而是一整套关于共享资源、执行时序、资源边界和系统稳定性的工程能力。

如果把全文压缩成几条最重要的结论,就是下面这些:

真正掌握并发,不是会背定义,而是你开始能在写业务代码时主动识别:

当你具备这种判断力时,并发才真正成为你的工程能力,而不是面试题。

常用代码片段汇总

synchronized

public synchronized void add() {
    count++;
}

ReentrantLock

lock.lock();
try {
    count++;
} finally {
    lock.unlock();
}

AtomicInteger

AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();

ThreadPoolExecutor

ExecutorService executor = new ThreadPoolExecutor(
        8, 16, 60L, TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(1000),
        Executors.defaultThreadFactory(),
        new ThreadPoolExecutor.CallerRunsPolicy()
);

CompletableFuture

CompletableFuture<User> userFuture =
        CompletableFuture.supplyAsync(() -> userService.getUser(userId), executor);

到此这篇关于面试与实战必备:掌握Java并发编程关键知识点(线程池/CAS/性能优化)的文章就介绍到这了,更多相关Java并发编程核心知识点梳理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文