Java 并发编程进阶,从线程池、锁、AQS 到并发容器与性能调优全解析

Java 并发编程进阶,从线程池、锁、AQS 到并发容器与性能调优全解析 文章目录前言一、为什么并发问题总是在生产环境暴露二、并发编程到底在解决什么1. 性能问题2. 正确性问题3. 可控性问题三、进程、线程、协程先区分清楚1. 进程2. 线程3. 协程四、并发 bug 的根源共享、竞争、时序1. 共享2. 竞争3. 时序不可预测五、Java 内存模型为什么必须理解1. 原子性2. 可见性3. 有序性六、volatile 的作用与边界volatile 适合什么场景volatile 不适合什么场景七、synchronized 的本质synchronized 适合什么场景八、ReentrantLock 为什么存在ReentrantLock 的优势什么时候用 ReentrantLock九、AQS 到底是什么十、synchronized 和 ReentrantLock 怎么选优先用 synchronized 的场景优先用 ReentrantLock 的场景十一、读写锁什么时候适合使用适合读写锁的场景不适合的场景十二、线程池为什么是并发系统的核心十三、为什么生产环境不建议直接用 Executors典型风险1. newFixedThreadPool2. newCachedThreadPool十四、线程池参数如何估算1. CPU 密集型任务2. IO 密集型任务3. 真正的调优原则十五、拒绝策略为什么不能忽略各自特点1. AbortPolicy2. CallerRunsPolicy3. DiscardPolicy4. DiscardOldestPolicy十六、Future、Callable、CompletableFuture 的使用场景1. Callable 和 Future2. CompletableFuture使用 CompletableFuture 的注意点十七、CAS 和原子类的意义CAS 的优点CAS 的限制十八、AtomicLong 和 LongAdder 怎么选AtomicLong 适合LongAdder 适合十九、并发容器如何选型1. ConcurrentHashMap2. CopyOnWriteArrayList3. BlockingQueue4. ConcurrentLinkedQueue二十、阻塞队列的重要性使用阻塞队列时要考虑什么二十一、死锁为什么难排查预防死锁的原则二十二、ThreadLocal 为什么常用又危险风险点二十三、并发性能优化中的常见误区误区 1线程越多越快误区 2锁一定很慢误区 3异步一定比同步高级误区 4CPU 高说明系统在努力工作误区 5用了线程池就万事大吉二十四、线上并发问题如何排查1. 先看症状2. 看线程池3. 看线程栈4. 看下游资源5. 看 GC6. 看业务设计二十五、一个典型线程池事故案例二十六、并发代码设计的硬规则1. 优先减少共享状态2. 显式创建线程池3. 所有共享变量都要明确线程安全策略4. 组合操作要特别谨慎5. 锁粒度尽量小6. 高并发热点计数优先用 LongAdder7. 线程池、锁、队列必须有监控8. 不要为了并发而并发二十七、如何高效学习 Java 并发1. 自己写小实验2. 结合源码学习3. 带着线上问题学习总结常用代码片段汇总synchronizedReentrantLockAtomicIntegerThreadPoolExecutorCompletableFuture互动话题前言Java 并发编程是后端开发中非常核心的一项能力。很多线上问题表面上看起来是“偶发故障”实际上根因都和并发有关比如线程池被打满锁竞争严重某些数据偶尔不一致接口响应时间抖动CPU 飙高但吞吐没有提升异步任务堆积死锁可见性问题并发容器误用很多人学习并发时停留在 API 层知道synchronized、volatile、ThreadPoolExecutor的写法但遇到线上问题时仍然很难定位和判断。这篇文章从工程视角出发系统梳理 Java 并发编程的关键知识点帮助你把并发能力真正转化成可落地的工程判断力。本文主要内容包括并发问题的本质Java 内存模型volatile 和 synchronizedLock 与 AQS线程池设计Future 与 CompletableFutureCAS 与原子类并发容器死锁与线上排查并发性能优化思路一、为什么并发问题总是在生产环境暴露并发问题有一个典型特点本地不容易复现线上高峰期集中爆发。原因很简单。并发 bug 往往和时序有关而时序又受到下面这些因素影响线程调度CPU 竞争GC 暂停网络波动数据库慢查询下游服务阻塞机器负载在本地开发环境下请求量小、线程数少、资源竞争弱很多问题根本暴露不出来。一旦进入高并发、长时间运行环境隐藏问题就会被放大。常见表现包括计数不准数据重复处理某些请求偶发失败响应时间忽高忽低线程池任务堆积线程数量异常增长CPU 占用高服务看起来没挂但几乎无响应这类问题如果没有并发基础很难真正定位。二、并发编程到底在解决什么并发编程的本质不是“让代码显得高级”而是为了在有限资源下提高吞吐、提升响应效率并确保结果正确。它主要解决三类问题1. 性能问题多个任务并行处理提高资源利用率。2. 正确性问题多个线程同时读写共享数据时如何保证结果不出错。3. 可控性问题线程数量、任务队列、锁竞争、资源消耗必须有边界。很多开发者只盯着“并发更快”但生产系统里更关键的是并发之后还能不能稳定、可控、可排查。三、进程、线程、协程先区分清楚1. 进程进程是资源分配的基本单位。一个 Java 应用启动后对应一个独立进程。2. 线程线程是 CPU 调度的基本单位。一个进程内部可以有多个线程这些线程共享堆内存、方法区等资源。3. 协程协程是更轻量级的执行单元通常在用户态调度。Java 传统并发主要以线程为核心不过现在虚拟线程也在逐步发展。对于大多数 Java 后端开发者来说目前最需要掌握的仍然是线程模型因为锁线程池并发容器AQS可见性问题这些核心知识都围绕线程展开。四、并发 bug 的根源共享、竞争、时序并发代码为什么难根本原因就在于这三件事同时存在1. 共享多个线程访问同一份资源。2. 竞争多个线程同时修改共享资源。3. 时序不可预测线程何时运行、何时切换、谁先谁后开发者很难完全控制。只要代码中出现“共享变量”就要立刻问自己三个问题会不会被多个线程访问有没有写操作写操作是否依赖顺序如果答案是“多个线程访问且至少一个线程会写”那就必须明确线程安全策略。五、Java 内存模型为什么必须理解并发问题很多时候不是代码逻辑错而是线程看到的变量值不一致。Java 内存模型也就是 JMM规定了线程如何与主内存交互线程何时能看到其他线程写入的值哪些操作能保证有序性和可见性理解 JMM至少要掌握三个概念1. 原子性一个操作是否不可分割。2. 可见性一个线程修改变量后另一个线程能否立刻看到。3. 有序性程序执行顺序是否和代码书写顺序一致。举个例子count;这不是原子操作它至少包括三步读取 countcount 1写回 count因此在多线程下会出现数据竞争。六、volatile 的作用与边界volatile的核心能力是保证可见性一定程度上禁止指令重排序示例privatevolatilebooleanrunningtrue;一个线程将running改成false另一个线程通常可以很快读到最新值。volatile 适合什么场景状态开关配置刷新标记一次写、多次读双重检查单例中的实例引用volatile 不适合什么场景计数器自增余额扣减复合条件判断依赖“读-改-写”原子性的逻辑例如下面代码依然线程不安全privatevolatileintcount0;publicvoidincrement(){count;}因为count不是原子操作。所以一定要记住一句话volatile 保证可见性不保证复合操作原子性。七、synchronized 的本质synchronized是 Java 最基础的内置锁。它能保证同一时刻只有一个线程进入临界区进入和退出同步块时具备可见性语义在同步范围内具备一定有序性保障示例publicsynchronizedvoidadd(){count;}或者synchronized(lock){count;}synchronized 适合什么场景简单互斥访问临界区较小对锁控制要求不复杂代码可读性优先很多开发者印象里觉得synchronized性能很差这个认知已经过时。JDK 对它做过大量优化很多场景下完全够用。八、ReentrantLock 为什么存在如果你对锁有更细粒度的控制需求ReentrantLock就比synchronized更适合。示例privatefinalReentrantLocklocknewReentrantLock();publicvoidadd(){lock.lock();try{count;}finally{lock.unlock();}}ReentrantLock 的优势支持可中断获取锁支持tryLock()支持超时获取锁支持公平锁支持多个Condition什么时候用 ReentrantLock需要超时等待锁需要中断等待锁需要多个条件队列需要更细致的锁控制但同时要注意显式锁一定要在 finally 中释放。九、AQS 到底是什么AQS全称AbstractQueuedSynchronizer是 Java 并发包里非常核心的同步框架。很多常见同步工具都建立在 AQS 之上例如ReentrantLockSemaphoreCountDownLatchReentrantReadWriteLockFutureTaskAQS 的核心思路可以概括为用一个state表示同步状态用等待队列保存竞争失败的线程线程获取资源失败后排队等待资源释放后唤醒后继节点理解 AQS 的意义不在于每天自己手写同步器而在于你能真正理解锁是怎么排队的线程为什么阻塞为什么有锁竞争为什么线程会被唤醒十、synchronized 和 ReentrantLock 怎么选简单来说可以这样判断优先用 synchronized 的场景只需要简单互斥临界区逻辑不复杂不需要尝试获取锁不需要超时控制更看重代码简洁优先用 ReentrantLock 的场景需要可中断需要 tryLock需要公平锁需要多个条件变量需要更细粒度锁控制不要为了“显得专业”把所有同步都替换成ReentrantLock。大多数场景简单的方案更稳。十一、读写锁什么时候适合使用如果一个共享资源是典型的“读多写少”读写锁通常能带来更好的并发能力。示例privatefinalReentrantReadWriteLockrwLocknewReentrantReadWriteLock();privatefinalLockreadLockrwLock.readLock();privatefinalLockwriteLockrwLock.writeLock();读操作readLock.lock();try{returncache.get(key);}finally{readLock.unlock();}写操作writeLock.lock();try{cache.put(key,value);}finally{writeLock.unlock();}适合读写锁的场景缓存配置读取字典数据查询远多于更新的共享资源不适合的场景写操作频繁临界区很小锁维护成本高于收益十二、线程池为什么是并发系统的核心生产环境里几乎不应该频繁手动new Thread()。原因很简单线程创建和销毁有成本线程数不受控会压垮系统线程切换也有成本缺乏统一管理和监控线程池的核心价值就是统一管理线程资源并建立明确边界。线程池的关键参数包括corePoolSizemaximumPoolSizekeepAliveTimeworkQueuethreadFactoryRejectedExecutionHandler示例ExecutorServiceexecutornewThreadPoolExecutor(8,16,60L,TimeUnit.SECONDS,newArrayBlockingQueue(1000),Executors.defaultThreadFactory(),newThreadPoolExecutor.CallerRunsPolicy());十三、为什么生产环境不建议直接用 Executors很多人习惯直接这样写Executors.newFixedThreadPool(10);或者Executors.newCachedThreadPool();这类 API 虽然方便但在生产环境通常不推荐直接使用原因是关键参数被隐藏了。典型风险1. newFixedThreadPool默认使用无界队列任务堆积时可能不断占用内存。2. newCachedThreadPool线程数增长过快时可能导致大量线程创建造成调度和资源压力。生产环境更合理的方式是显式使用 ThreadPoolExecutor把线程数、队列长度、拒绝策略写清楚。十四、线程池参数如何估算线程池没有万能配置但可以从任务类型入手。1. CPU 密集型任务例如加密解密规则计算图像处理复杂算法这类任务线程数通常接近 CPU 核数。经验值CPU核数 12. IO 密集型任务例如数据库访问Redis 调用HTTP 调用文件读写这类任务线程经常在等待可以适当提高线程数。3. 真正的调优原则线程池参数不能只靠经验公式最终一定要看接口响应时间队列堆积情况拒绝次数CPU 使用率下游资源瓶颈十五、拒绝策略为什么不能忽略线程池不是无限吞任务的。当线程数达到上限、队列也满了之后线程池会触发拒绝策略。JDK 常见拒绝策略有四种AbortPolicyCallerRunsPolicyDiscardPolicyDiscardOldestPolicy各自特点1. AbortPolicy直接抛异常。适合需要明确感知失败的场景。2. CallerRunsPolicy由提交任务的线程自己执行适合做一定程度的反压。3. DiscardPolicy直接丢弃任务不抛异常。高风险不适合关键任务。4. DiscardOldestPolicy丢弃最早排队的任务。是否合理取决于业务语义。最危险的不是哪种策略不好而是系统已经在拒绝任务了但没人知道。所以拒绝次数一定要纳入监控。十六、Future、Callable、CompletableFuture 的使用场景1. Callable 和 FutureCallable相比Runnable支持返回值和异常。示例FutureIntegerfutureexecutor.submit(()-12);Integerresultfuture.get();问题在于Future组合能力差多任务编排麻烦异常传播不优雅2. CompletableFuture如果需要做异步编排CompletableFuture更适合。例如CompletableFutureUseruserFutureCompletableFuture.supplyAsync(()-userService.getUser(userId),executor);CompletableFutureListOrderorderFutureCompletableFuture.supplyAsync(()-orderService.getOrders(userId),executor);CompletableFutureUserDetailDTOresultFutureuserFuture.thenCombine(orderFuture,UserDetailDTO::new);使用 CompletableFuture 的注意点生产环境尽量显式指定线程池不要把所有业务都异步化要明确异常处理链路不要忽略下游资源瓶颈十七、CAS 和原子类的意义CAS 是 Compare-And-Swap通过比较当前值与期望值是否一致来决定是否更新。Java 中常见原子类包括AtomicIntegerAtomicLongAtomicReferenceLongAdder示例AtomicIntegercounternewAtomicInteger(0);counter.incrementAndGet();CAS 的优点轻量低冲突下性能好无需传统互斥锁CAS 的限制高冲突时会反复重试会消耗 CPU不适合复杂临界区逻辑存在 ABA 问题所以原子类适合做计数器状态位切换简单引用更新不适合直接替代所有加锁场景。十八、AtomicLong 和 LongAdder 怎么选AtomicLong 适合并发不高单点计数需要简单直接LongAdder 适合高并发热点计数指标统计访问次数累加监控场景原因是LongAdder会分散竞争热点最后再汇总结果在高并发统计场景下通常更有优势。十九、并发容器如何选型常见并发容器包括ConcurrentHashMapCopyOnWriteArrayListConcurrentLinkedQueueBlockingQueue1. ConcurrentHashMap适合缓存、本地索引、状态映射。ConcurrentHashMapString,ObjectcachenewConcurrentHashMap();2. CopyOnWriteArrayList适合读多写少场景比如监听器列表。3. BlockingQueue适合生产者消费者模型、异步任务缓冲。4. ConcurrentLinkedQueue适合高并发无阻塞队列场景。这里一定要记住一句话并发容器保证的是单次容器操作线程安全不保证多个组合操作天然线程安全。例如下面写法就不是绝对安全的if(!map.containsKey(key)){map.put(key,value);}应优先使用map.putIfAbsent(key,value);或者map.computeIfAbsent(key,k-createValue(k));二十、阻塞队列的重要性阻塞队列在并发系统里非常关键尤其在线程池和生产者消费者模型中。常见阻塞队列有ArrayBlockingQueueLinkedBlockingQueueSynchronousQueueDelayQueuePriorityBlockingQueue使用阻塞队列时要考虑什么是否需要有界是否允许任务堆积是否需要优先级是否是延迟任务是否需要直接移交这里最重要的一个原则是高并发系统优先考虑有界队列。无界队列会把问题“延后爆炸”而不是解决问题。二十一、死锁为什么难排查死锁通常发生在多个线程相互等待对方释放资源时。典型场景线程 A 持有锁 A等待锁 B线程 B 持有锁 B等待锁 A示例synchronized(lockA){synchronized(lockB){}}另一段代码反过来synchronized(lockB){synchronized(lockA){}}预防死锁的原则统一加锁顺序减少锁嵌套缩小锁粒度避免持锁期间做远程调用必要时使用 tryLock timeout线上排查死锁最常用的手段是jstack二十二、ThreadLocal 为什么常用又危险ThreadLocal用来为每个线程保存独立变量副本。适合场景当前登录用户上下文traceId请求级上下文格式化对象缓存示例privatestaticfinalThreadLocalStringCURRENT_USERnewThreadLocal();风险点如果在线程池环境中使用ThreadLocal但没有及时清理线程复用后就可能读到旧值甚至导致内存泄漏。正确写法try{CURRENT_USER.set(userId);// 业务逻辑}finally{CURRENT_USER.remove();}结论很明确线程池环境中使用 ThreadLocal一定要 remove。二十三、并发性能优化中的常见误区误区 1线程越多越快错误。线程太多会带来上下文切换和资源竞争。误区 2锁一定很慢错误。低竞争场景下锁可能非常高效。误区 3异步一定比同步高级错误。异步只是改变执行方式不一定更优。误区 4CPU 高说明系统在努力工作错误。高 CPU 可能是自旋、死循环、过度序列化、GC 或竞争。误区 5用了线程池就万事大吉错误。线程池参数配置、队列长度、拒绝策略、任务拆分方式都会决定最终效果。并发优化必须建立在真实监控和压测数据之上而不是凭感觉。二十四、线上并发问题如何排查排查并发问题时建议按下面顺序看。1. 先看症状是响应慢还是错误率高还是吞吐下降还是 CPU 飙高还是线程数过多2. 看线程池活动线程数队列长度拒绝次数最大线程数是否打满3. 看线程栈使用jstack查看是否有大量 BLOCKED 线程是否有 WAITING 线程堆积是否有死锁是否有长时间卡在 IO4. 看下游资源数据库连接池是否耗尽Redis 是否超时HTTP 下游是否变慢5. 看 GC长时间 STW 也会放大并发问题表象。6. 看业务设计是否线程池共用导致互相影响是否任务拆分太碎是否热点 key 导致竞争是否异步任务没有边界二十五、一个典型线程池事故案例假设一个订单系统一个请求会触发这些动作查用户查库存创建订单发短信发通知写日志更新推荐系统开发者为了“提升性能”把后面所有动作都异步化丢进同一个线程池。线程池参数如下核心线程 20最大线程 200无界队列低峰期没问题高峰期库存服务一慢异步任务消费速度下降。因为队列无界任务会不断积压。短时间内看不出异常但随着任务越堆越多内存、GC、延迟都会变差最后系统整体雪崩。这个案例说明几个关键问题线程池不能混用所有业务无界队列非常危险下游慢必须有超时和降级线程池必须监控异步并不等于高性能二十六、并发代码设计的硬规则下面这些原则非常实用。1. 优先减少共享状态不共享就没有竞争。2. 显式创建线程池不要依赖默认线程池或偷懒工厂方法。3. 所有共享变量都要明确线程安全策略要么锁、要么原子类、要么线程封闭、要么不可变。4. 组合操作要特别谨慎容器线程安全不代表业务逻辑整体线程安全。5. 锁粒度尽量小持锁期间不要做慢操作、远程调用、数据库操作。6. 高并发热点计数优先用 LongAdder不要让所有线程争抢同一个热点原子变量。7. 线程池、锁、队列必须有监控不可观测的并发系统基本不可维护。8. 不要为了并发而并发如果真正瓶颈在数据库或网络多开线程不一定有效。二十七、如何高效学习 Java 并发学习并发最有效的方式不是死记硬背而是结合代码和问题去理解。建议从三个方向入手。1. 自己写小实验例如多线程计数器volatile 可见性测试synchronized 和 AtomicInteger 对比线程池参数实验2. 结合源码学习重点建议看ThreadPoolExecutorConcurrentHashMapReentrantLockCountDownLatchCompletableFuture3. 带着线上问题学习比如为什么线程池会堆积为什么接口偶发超时为什么 CPU 高但吞吐没上去为什么会死锁有问题驱动理解会更扎实。总结Java 并发编程不是几个 API 的使用技巧而是一整套关于共享资源、执行时序、资源边界和系统稳定性的工程能力。如果把全文压缩成几条最重要的结论就是下面这些线程安全的核心不是加锁而是控制共享和竞争volatile 解决可见性不解决复合操作原子性简单互斥优先 synchronized复杂控制再考虑 Lock线程池必须显式配置参数和队列一定要可控并发容器保证单次操作安全不保证复合逻辑天然安全并发优化必须基于监控和压测不要凭感觉调整线上并发问题本质上往往是资源模型和边界设计问题真正掌握并发不是会背定义而是你开始能在写业务代码时主动识别哪些状态会共享哪些地方会竞争哪些操作需要隔离哪些任务需要限流哪些线程池必须拆分哪些锁会成为瓶颈当你具备这种判断力时并发才真正成为你的工程能力而不是面试题。常用代码片段汇总synchronizedpublicsynchronizedvoidadd(){count;}ReentrantLocklock.lock();try{count;}finally{lock.unlock();}AtomicIntegerAtomicIntegercounternewAtomicInteger(0);counter.incrementAndGet();ThreadPoolExecutorExecutorServiceexecutornewThreadPoolExecutor(8,16,60L,TimeUnit.SECONDS,newArrayBlockingQueue(1000),Executors.defaultThreadFactory(),newThreadPoolExecutor.CallerRunsPolicy());CompletableFutureCompletableFutureUseruserFutureCompletableFuture.supplyAsync(()-userService.getUser(userId),executor);互动话题你在项目里遇到过哪些并发问题线程池打满锁竞争严重数据不一致死锁CPU 飙高异步任务堆积可以在评论区交流。