新闻详情

新闻详情

首页 / 资讯中心 / 详情

Java并发编程实战:从内存模型到线程池调优与故障排查

发布时间:2026/9/28 23:27:41来源:尧图网络
Java并发编程实战:从内存模型到线程池调优与故障排查
开篇为什么这块硬骨头值得啃下来做了好几年后端开发我越发觉得并发编程像一门“玄学”和“工程学”的混合体。你可以在十分钟内把线程跑起来但想要让它在高并发下不崩溃、不卡顿、不出错却往往要花上几天甚至几周的时间去打磨。很多朋友一提到多线程就头皮发麻面试时对着 volatile 和 synchronized 背诵一些标准答案到了线上排查问题时却依然一头雾水。这篇笔记我会按照自己的学习路径和实战经验来写从底层原理讲到代码落地再讲到线程池配置和线上问题的排查。内容更适合已经用过一些并发工具、但还没有建立起完整体系的朋友当然只要你能看懂基本的 Java 语法跟着思路走一遍也会收获很多。我最初接触并发编程时其实是有些抵触的。觉得单线程很自然多线程一旦加进来各种奇奇怪怪的 bug 就开始出现了——数据对不上、程序卡死、CPU 飙升甚至有时候程序重启才能恢复。后来我才慢慢意识到这些问题并不是并发本身带来的而是我根本不了解并发的三大核心问题可见性、原子性和有序性。搞清楚这三点再去看任何并发框架和工具你会发现它们的设计思路其实一脉相承都是为了让多线程访问共享资源时内存的数据是一致的、代码的执行顺序是可控的、关键的操作是不可分割的。这篇笔记我不打算像教科书那样面面俱到只挑那些真正耽误过我的坑、真正帮我解决问题的内容来聊。篇幅会有点长但每一节都能落回到代码和实战上。1. 并发编程的底层逻辑与核心痛点1.1 三个问题可见性、原子性、有序性所谓“并发”本质上就是多个线程同时访问共享资源。而 CPU、内存和代码编译器这三者之间存在着一道道不太“听话”的中间层。最先让我踩坑的是可见性问题。你可能会觉得一个变量在线程 A 里改成了 true线程 B 里立刻就能看到。但实际上线程 B 看到的很可能还是旧的 false。原因是 CPU 缓存的存在——每个核心都有自己的缓存线程 A 修改的是核心缓存中的值还没同步回主内存线程 B 读到的自然是旧值。我用一个简单的循环加标记位的例子来解释主线程设置一个 flag 来通知子线程退出循环如果没有 volatile 修饰某些 JDK 版本下子线程可能一直死循环下去标记位的修改对它来说“不可见”。然后是原子性问题。一个 i 看起来是一行代码但在 CPU 层面它实际上拆成了“读取 i、计算 i1、写回 i”三步。如果两个线程同时执行 i结果很可能比预期少一次。这是初学并发时最容易犯的错。自增操作不是原子的就像两个人同时在一张纸上写同一个数字的累加结果最终纸上的数字大概率是不对的。解决原子性问题的手段就是给这段“读-改-写”的路径加锁让它在同一时刻只被一个线程执行。最后是有序性问题。编译器为了提高性能会做指令重排这在单线程下不影响最终结果但在多线程下就有可能出现“看似不可能发生”的现象。比如线程 A 依次执行“设置某状态1、设置 flagtrue”线程 B 看到 flag 为 true 后去读取某状态结果可能读到的是初始的 0。这就是典型的指令重排导致的问题。我们在使用双重检查锁的单例模式时那个 instance 变量必须加 volatile正是为了禁止对象引用赋值时的指令重排。背后靠的是内存屏障它像栅栏一样阻止屏障前后的指令乱序执行。经验之谈遇到任何诡异的并发 Bug先按这三个问题去归类读不到最新值可见性、结果比预期少原子性、现象不合逻辑有序性。绝大多数问题都能找到对应的解决方法。1.2 并发不是银弹什么时候该用多线程很多团队一遇到性能瓶颈第一反应是“加线程”觉得线程越多处理越快。实际上并发的收益是有边界的。如果一个任务本身是计算密集型的也就是说它一直在占用 CPU 做计算那么线程数超过 CPU 核心数之后只会增加线程上下文切换的开销吞吐量不升反降。如果一个任务是 IO 密集型的比如频繁读写数据库、调用远程接口、读写文件线程在执行 IO 操作时会进入阻塞状态CPU 其实是空闲的这时适当增加线程数可以提升利用率。在我自己负责的一个订单查询服务中就遇到过这个选择。最初把线程池核心线程数直接调到 CPU 核数的两倍压测发现吞吐只有原来的 80%。后来分析了一下大部分时间都在查数据库和调用外部接口于是按 IO 密集型的公式重新计算线程数 CPU 核数 × (1 线程等待时间 / 线程计算时间)。调整之后吞吐量明显提升。这个公式只是估算但它至少比拍脑袋靠谱得多。核心思想是算法和数据结构的优化往往比创建更多线程更有效多线程是为了压榨硬件利用率而不是无脑堆资源。2. 线程生命周期与代码落地细节2.1 线程状态流转别再靠背了线程状态是面试高频题但很多人的理解只停留在名字上。Java 线程一共六种状态NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。NEW 是刚创建还没调用 start。RUNNABLE 是就绪或运行中。BLOCKED 是等待进入 synchronized 同步块。WAITING 是等待被显式唤醒。TIMED_WAITING 是带时间的等待。TERMINATED 是执行完毕或异常退出。让我用生活场景类比一下。线程类似餐厅服务员。NEW 是他刚到店还没换上工服。RUNNABLE 是他已经能干活了可能在走菜也可能在等传菜指令。BLOCKED 是几个服务员同时想进仓库拿餐具挤在门口互相等待。WAITING 是他把菜送出去后在角落里发呆等客人按铃叫他。TIMED_WAITING 是他摸了摸鱼规定自己 5 分钟后去检查餐桌。TERMINATED 就是下班走人了。实际写代码时最常用的排查手段是 jstack 命令抓线程快照。线上出现了线程堆积我通常会抓两份快照做对比看线程一致卡在哪些方法上。有一次发现大量线程停在 HttpClient 的连接池获取连接上排查之后是连接池太小大量请求在排队等待通过调大连接池参数解决问题。状态流转的知识往往就是这样和故障排查联系在一起。2.2 从 Thread 到 Executor创建线程的四种姿势第一种直接继承 Thread 覆盖 run 方法。这种方式简单直接但 Java 是单继承扩展性很差一般情况下不推荐。第二种实现 Runnable 接口把任务逻辑传给 Thread。好处是解耦了任务和线程还能借助外部类共享变量但 Runnable 的 run 方法没有返回值无法拿到执行结果。第三种是实现 Callable 接口并配合 FutureTask。Callable 的 call 方法可以有返回值也可以抛出异常。实际开发中我经常会用到它去并发查询多个数据源再汇总结果。例如同时查库存、查价格、查优惠信息三个任务并行执行最后用 future.get() 逐个取出结果。这里要注意 get 方法是阻塞的如果某个任务一直不返回当前线程会一直等下去所以最好用带超时的 get 版本。第四种是线程池 ExecutorService这也是生产环境中最推荐的。线程池把线程的创建和销毁统一管理起来既复用线程又能通过队列承担缓冲。从 JDK 5 开始的 Executors 工具类可以快速创建几种线程池但实际工程中更建议大家直接用 ThreadPoolExecutor 构造方法去创建因为这样可以精确控制核心线程数、队列大小、拒绝策略等参数。后面章节我会详细展开这部分的内容。2.3 start 和 run 的语义差别我当年也搞混过刚入行时我写过一个线程调用的是 run 而不是 start结果程序完全没变快。因为直接调用 run 方法就是普通方法调用并没有创建新线程而是在当前线程里同步执行了任务。只有调用 start 才会让 JVM 创建新的线程栈然后回调 run 方法。这个坑虽然基础却非常容易犯。有一次我甚至在代码评审中看到有人把线程池提交的 Runnable 对象直接执行了 run 方法那和单线程执行毫无区别。这个问题背后也涉及线程启动的重复性一个线程只能启动一次。再次调用 start 会抛出 IllegalThreadStateException。如果你试图用同一个线程对象去执行多个任务那是行不通的。任务和线程应该是一一对应的或者把任务交给线程池去复用线程而不是重用一个 Thread 对象反复 start。3. 深入 JMM 与同步原语从 volatile 到 synchronized3.1 Java 内存模型和 happens-before 规则为什么有时候加了 volatile 就能解决可见性有时候问题还在这就要提到 Java 内存模型JMM。JMM 是 Java 定义的内存抽象规范规定了线程读写变量的规则。简单来说每个线程有自己的工作内存线程对变量的操作都是先操作工作内存再同步回主内存。JMM 要解决的问题就是在什么情况下一个线程对变量的修改对另一个线程是可见的这就是 happens-before 规则。happens-before 规则里有很多条我不打算全列出来只挑开发中最常用的几条。程序次序规则单线程内按代码顺序执行。锁规则一个 unlock 操作 happens-before 后续对同一个锁的 lock 操作。volatile 规则对一个 volatile 变量的写操作 happens-before 后续对这个变量的读操作。传递性如果 A happens-before BB happens-before C那么 A happens-before C。这些规则不是玄学它们是从语言层面做出的承诺保证程序员在遵守规则的前提下编写的代码不会出现不可预测的内存行为。举个例子你在线程 A 中先修改了一个普通变量 userId再往一个 volatile 变量 ready 写入 true线程 B 不断轮询 ready发现它为 true 后再去读 userId。根据传递性和 volatile 规则线程 B 一定能够看到线程 A 对 userId 的修改。这就是很多“无锁”通信方案的基础设计。当然这里每一步都要小心不能违背规则否则就回到不可预测的状态。3.2 volatile 能做什么绝对不能做什么volatile 经常被误解为“原子变量”其实它只能保证可见性和有序性不具备原子性。我用它最多的场景是作为状态标志位、发布不可变对象、配合双重检查锁。遇到计数累加、多线程写同一份数据这类场景volatile 完全不能替代锁或原子类。举例来说一个电影网站的播放次数统计如果直接用 volatile 修饰一个 int 变量再去 i并发一高次数就会出现漏加。因为 i 是三步操作volatile 只是保证读的时候是最新值但在读和写之间的间隙里其他线程依然可能改了同一个变量导致 写覆盖。这类统计最好用 AtomicInteger 或者加锁甚至通过数据库的乐观锁机制来实现。另外补充一个细节volatile 在 Java 5 之前的模型中语义并不完整。Java 5 之后 JMM 重新定义后volatile 的语义才足够可靠。所以如果你维护的是老年代码见到关于 volatile 的“某些极端情况下仍出问题”的讨论多半是 JDK 版本太老导致的现代 JDK 可以放心使用。但放心不代表滥用用对场景是前提。3.3 synchronized 的升级过程和锁消除synchronized 是最经典的同步手段但当初我对它有个误解以为它一定是重量级锁。后来了解到锁升级机制后才知道 JVM 做了很多优化。无锁状态下JVM 会先尝试偏向锁让同一个线程反复进入同步块时不需要竞争一旦有第二个线程参与竞争偏向锁会升级为轻量级锁通过自旋等待来避免线程上下文切换如果自旋失败再升级为重量级锁交由操作系统线程调度。这个过程是自动完成的。这个设计背后的理念是绝大多数锁只被少数线程访问过度设计重量级机制反而浪费资源。我在生产环境里曾经见过一段代码在循环内同步访问 HashMap锁竞争虽然存在但因为锁块很小轻量级锁自旋就解决了大部分竞争性能尚可。这提醒我不要一听到 synchronized 就觉得性能差得结合场景评估。锁粒度、锁持有时间、并发线程数才是决定性能的关键。synchronized 还支持三处使用方式同步实例方法、同步静态方法、同步代码块。前两者锁定的对象分别为当前实例和当前类的 Class 对象代码块则需要显式指定锁对象。有时候我会用类 Class 对象作为锁因为不同实例之间也需要互斥访问同一份静态资源。但要注意如果锁对象选择不当比如用了一个可能为 null 的字符串常量可能在不同位置用了同一个字符串常量导致互相干扰这种隐蔽问题我在代码审查中见过几次。3.4 锁优化的核心思路减少持有时间、降低竞争频率积累了一些经验之后我开始主动做锁优化本质就两个方向减少持锁时间、降低竞争烈度。减少持锁时间最常用的做法是缩小同步块范围。比如一个方法里面既有耗时的计算又有需要互斥的共享资源修改把同步块写在修改那一小段代码外面而不是包住整个方法体。降低竞争频率的思路是用锁分段或者读写分离。经典的 ConcurrentHashMap 分段锁思路后来被 CAS 操作取代但思想很有启发——不同数据段使用不同锁互不干扰。读写锁 ReentrantReadWriteLock 和 StampedLock 也很实用读多写少场景用读写锁读和写都能并发只有写和写之间互斥。当读操作非常频繁且存在大量写线程时StampedLock 的乐观读能够做到近乎无锁的读流程但使用复杂度和风险都更高。我自己的一个实际经验是业务代码里千万不要在持锁状态下调用外部接口。当时有个项目在 synchronized 代码块里调用第三方支付接口响应越来越慢所有线程都堆在锁上整个系统几乎不可用。后来改成先查本地数据再锁内部状态更新锁很快释放问题才缓解。这就是典型的“锁持有时间过长”。4. 从锁到并发工具J.U.C 包的核心武器4.1 Lock 的价值与 ReentrantLock 的实战用法synchronized 能做到的事很多但有些场景它做起来很别扭比如可中断获取锁、尝试获取锁、公平锁策略。这些都是 Lock 接口和它的实现类 ReentrantLock 的强项。Lock 用 tryLock 可以避免线程无限期阻塞用 lockInterruptibly 可以响应中断在复杂的异步场景下价值很大。我维护过一个任务调度模块多个线程都想抢到“凌晨 2 点跑报表”的任务但同一时间只允许一个任务执行。使用 ReentrantLock 的 tryLock 方法抢不到锁的线程直接放弃不排队等待比让所有线程都阻塞在锁上要好得多。使用 Lock 时必须注意在 finally 里解锁这是和 synchronized 最大的区别。synchronized 的锁是自动释放的Lock 一旦忘记 unlock相当于线程持有锁不释放后续所有需要这个锁的线程都会卡死。ReentrantLock 还有一个公平模式new ReentrantLock(true) 会按照线程到达的先后顺序分配锁。默认的非公平模式是允许线程“插队”的好处是减少了空当提升了吞吐量但可能存在线程饥饿的隐患。实际业务中对公平性有强需求的场景并不多我自己只在一些凭证生成模块里用到公平锁因为这个场景对“序号严格递增”有硬性要求。4.2 并发容器的选择与现实坑点并发容器是实战中最常用的工具之一。我最常用的是 ConcurrentHashMap、CopyOnWriteArrayList 和 BlockingQueue 系列。ConcurrentHashMap 在很多版本里读操作完全无锁写操作只锁住 bucket支持并发读与高并发写。它不能接受 null 的 key 或 value这一点和 HashMap 完全不同。如果你往里面 put 一个 null其他线程 get 时返回 null你根本无法区分是“取不到值”还是“值为空”这样会产生歧义。CopyOnWriteArrayList 是读多写少场景的利器它的“写时复制”机制让读不需要加锁但每次写都会复制整个数组。如果写频繁复制旧数据的开销会大到让你怀疑人生。有一次我用它存用户的在线状态更新频繁最后复制了大量数组对象GC 压力飙升换了 ConcurrentHashMap 才缓解。BlockingQueue 里最常用的是 ArrayBlockingQueue 和 LinkedBlockingQueue。前者有界后者默认无界。生产环境中无界队列的风险很大因为一旦消费者跟不上消费者的速度队列会无限增长内存迟早耗尽。所以使用线程池时一定要用有界队列并配合明确的拒绝策略。4.3 信号量、倒计时器和循环栅栏三个同步辅助类Semaphore 有点像一个有令牌的停车场只有拿到令牌的线程才能进入指定区域。初始化的令牌数量就是允许并发的线程数。使用 Semaphore 可以限制某个核心区间的并发访问量比如一次性查询数据库的最大连接占用数量。我在一个导出大文件的模块里用 Semaphore 控制并发导出任务数防止同时有几百个导出任务把内存耗尽。acquire 方法的调用要放在 try-finally 中release 确保一定执行否则令牌会越来越少最后所有请求都会被卡住。CountDownLatch 非常适合“等待多个任务完成后再继续”的场景。核心木马者裁判员子任务每完成一个就 countDown 一次主线程 await 直到计数归零。注意 CountDownLatch 的计数不能重置一旦计数到零这个实例就废了如果有“分批等待下一轮”的需求就要换用 CyclicBarrier。CyclicBarrier 是可以循环使用的它更强调“多个线程互相等待到齐后再一起出发”。举个例子批量导入文件时读取多个分片的任务分布在不同线程中主线程要等所有分片都读完才能开始合并数据CountDownLatch 就可以胜任。CyclicBarrier 比较适合比如“每轮比赛 5 位选手同时开跑等待所有选手抵达后再开始下一轮”的流程。两者的核心区别是 CountDownLatch 是一次性的、由外部线程等待CyclicBarrier 是可循环的、由参与线程互相等待。如果你需要实现反复同步多轮任务就用 CyclicBarrier 而不是重复 new 一个 CountDownLatch。4.4 Future 与 CompletableFuture异步结果收集的演进Future 是最初的异步结果容器get() 会阻塞等待结果。但 Future 有个明显的短板多个任务之间有依赖关系时编排起来非常痛苦你得手动去 get 完 A 再提交 B。CompletableFuture 就是为了解决这种依赖编排而生的它支持回调函数和组合式异步编程。例如“先查用户信息再根据用户信息查询订单最后汇总两者”这种链路CompletableFuture 可以写得很优雅甚至能指定不同的线程池去执行不同的阶段。但 CompletableFuture 有个容易被忽视的坑默认使用 ForkJoinPool.commonPool 执行任务如果在 Web 应用里大量使用所有任务的线程池都是共享的一个耗时接口就可能拖慢整个应用的其他异步任务。我通常在构造时显式传入一个专用线程池避免互相影响。另外complete 方法可以手动结束一个 future 并给结果这在超时处理时很有用如果原始异步任务迟迟不返回注册一个定时任务去 complete 一个“默认结果”就不至于让调用方无限等待。5. 线程池的工程化使用与参数调优5.1 ThreadPoolExecutor 的七个参数逐个拆解线程池是并发开发中最常见的生产工具。我之前看到有些同学直接在代码里写 Executors.newFixedThreadPool(10)图个方便。但生产中我更推荐直接使用 ThreadPoolExecutor 构造方法。七个参数各有含义核心线程数、最大线程数、空闲存活时间、时间单位、工作队列、线程工厂、拒绝策略。核心线程数是池中常驻线程数量即使空闲也不会回收。最大线程数是当队列满了之后允许额外创建的线程数量上限。空闲存活时间是线程空闲多久之后会被回收仅当线程数超过核心线程数时才有效。工作队列有两种典型方向直接交接队列 SynchronousQueue 和无界队列 LinkedBlockingQueue生产环境推荐用有界队列 ArrayBlockingQueue方便明确拒绝行为。线程工厂用来设置线程的命名前缀和是否设置为守护线程好的线程命名对排查问题极其重要。拒绝策略是当线程池和队列都满的时候如何处理新提交的任务。默认的 AbortPolicy 直接抛异常而 CallerRunsPolicy 会把任务交回调用方线程执行这个策略非常适合想要“不想丢弃任务、又不想压垮系统”的场景。我最常用的配置是核心线程数cpu 核心数1或者按 IO 密集型的公式估算最大线程数设置为核心线程数的两倍左右队列容量设置为一两百个任务。比如一个充值服务我配置 corePoolSize 为 8maxPoolSize 为 16队列容量为 200。在压测数据支持下这个配置既没有频繁拒绝请求也没有造成线程堆积。5.2 识别任务特性与选择拒绝策略线程池参数没有一个通用的“最佳值”只能根据任务特性和系统瓶颈来分析。计算密集型的任务线程数接近 CPU 核心数即可IO 密集型则要更多线程来掩盖阻塞时间。任务类型的不同还会影响队列的选择延迟性任务适合有界短队列因为队列里的任务等待太久会失去意义吞吐量优先的长任务可以接受较长的等待排队队列可以适当大一些。拒绝策略的选择也和业务性质强相关。如果我做一个流量高峰时宁可限流也不愿堆积延迟的系统我会选择 DiscardPolicy 或者自定义拒绝逻辑直接丢掉任务并记录日志。而 CallerRunsPolicy 则更优雅一点任务回落到调用方线程执行虽然会让调用方线程变慢但等于自然限流。DiscardOldestPolicy 会丢弃队列中最老的未处理任务适合那种“最新任务更重要”的场景比如实时数据更新。5.3 用线程池的坑线程池关闭与异常处理线程池使用过程中的一个隐蔽问题是异常的“吞掉”现象。submit 提交任务时如果任务内部抛异常异常会被封装在 Future 里面除非你调用 future.get()否则根本察觉不到异常。execute 则会在调用线程里输出异常栈。我建议在线程池任务内部自己捕获异常并记录业务日志不要依赖线程池框架的异常上报。关闭线程池时也不能马虎。shutdown() 会等正在执行的任务和队列里的任务全部结束不会立刻终止而 shutdownNow() 会尝试中断正在执行的线程并返回尚未开始的任务列表。如果我需要在 JVM 退出前优雅地处理完一批数据我会使用 shutdown 并等待指定的超时时间如果超时还没完成就记录日志。如果直接用了 shutdownNow 去收尾很可能导致一批任务执行一半数据状态不一致。重点提醒写线程池任务时永远要想清楚“如果有人提交任务后进程崩溃怎么办”。没有持久化保护的任务在线程池模式下同样会丢失。线程池只是解决资源复用并不能替代事务和消息队列。6. 并发场景的常见问题与排查技巧实录6.1 死锁定位与 arthas、jstack 使用死锁是所有线程都在等待一个不可能被释放的锁表现为程序卡死、吞吐量为零。排查死锁最经典的方法是抓线程快照。我常用 jstack 命令直接打印 JVM 线程栈或者使用 arthas 这个诊断工具一键导出所有线程堆栈和锁信息。定位到死锁后首先要看线程栈里是否出现“Found one Java-level deadlock”字样如果有它会列出两个线程各自持有的锁和等待的锁。解决方式有两种一是调整锁顺序让所有线程都按照相同的顺序加锁二是使用 tryLock 超时获取锁避免无限等待。我在多把锁嵌套的代码里强烈推荐使用 tryLock 配合超时时间即使超时后获取锁失败代码也可以走重试或补偿逻辑比“死等”优雅得多。6.2 竞态条件和数据不一致的排查思路竞态条件是指程序的结果依赖于线程执行顺序而这个顺序是不可控的。常见的竞态场景包括“先检查后执行”“读-改-写”操作、惰性初始化等。排查竞态条件首先需要确定共享变量有哪些然后判断这些变量是否被多线程同时读写。最简单的验证方法是加锁压测如果加上锁之后问题消失了那基本就能锁定是竞态问题。有一次我在一个库存扣减流程中遇到超卖问题。代码逻辑是先查库存再判断是否大于已购数量。并发用户同时提交订单时两个线程可能同时读到库存为 1同时判断可以购买就都扣减成功了。解决方法是把“查库存、判断、扣减”放进一个原子操作里用数据库乐观锁或 Redis 分布式锁都可以。核心思想是不要在业务代码外做“先查再改”关键链路要保住原子性。6.3 线程泄漏与内存溢出的现场恢复线程泄漏是另一个容易忽视的问题。每次创建新线程执行任务但线程没有正常结束或者线程池中的线程被异常中断却没有回收时间一长线程数量持续增长最终导致无法创建新线程或者内存溢出。排查线程泄漏最直观的方式是用 jstack 连续抓几次快照统计线程总数和各状态的线程数量。如果线程持续增长且都阻塞在一些 create、new 或等待的位置那就是泄漏的线索了。我维护过的一个单体应用中曾经发生过内存溢出的现象最终定位到是无限创建线程导致每个线程都保留一份线程栈和上下文引用GC 无法回收堆内存越占越多。当时紧急加了线程池线程数上限和队列上限并将调用方超时时间缩短系统才算恢复了稳定。事后反思线程池没有上线限、任务队列没有上限这两个配置失误实属不该。6.4 CPU 飙高、响应变慢的并发相关特征CPU 飙高往往不是因为业务线程本身而是因为频繁的上下文切换、自旋锁竞争、死循环等。有一次生产环境 CPU 持续接近 100%当时先看了线程 dump发现大量线程处于 RUNNABLE 状态并且在同一个加密算法的方法上运行。原因是每次请求都会生成一个全新的加密上下文频繁创建对象导致大量计算和 GC。改为复用上下文后CPU 立刻降了下来。响应变慢则需要结合线程池的状态来看。如果大量请求堆积在队列里响应时间必然上升同时线程数可能稳定在一个高位。此时需要关注的是队列入队出队的耗时、线程池是否处于饱和状态、任务是否有热点阻塞。我习惯在监控面板上同时看线程池活动线程数、队列深度和任务耗时三个指标任何一个异常都能快速定位到瓶颈区域。6.5 经验速查与避坑清单最后整理一份我在项目组里经常分享的并发编程速查清单每一条都来自真实踩坑后的复盘。场景推荐方案推荐理由多个线程读写一个标记位volatile禁止重排序保证可见性多线程累加计数AtomicLong / LongAdder无锁化降低自旋开销并发扣减库存数据库行锁或分布式锁保证原子性避免超卖读多写少的数据结构CopyOnWriteArrayList读无锁写复制高并发读写 mapsConcurrentHashMap并发度好避免锁表等待多任务完成CountDownLatch一次性计数逻辑清晰多轮线程同步出发CyclicBarrier可循环使用适合多轮任务限制接口并发量Semaphore令牌数量可动态调整异步任务编排链路CompletableFuture支持组合回调优雅处理前后依赖需要超时等待锁ReentrantLock.tryLock避免死锁具备中断能力避坑清单上最重要的几条是这样第一不要在持锁状态下调用外部接口第二不要用 Executors.newCachedThreadPool 处理突发流量它创建的线程数没有上限流量冲击容易直接把系统打垮第三共享变量使用前命名就带上并发语义比如 volatileFlag、atomicCount这样团队里的人一看就明白第四自写并发代码时一定要明确说明内存可见性语义的保证来源——是基于 volatile、加锁还是利用不可变对象你可别在代码里自己幻想着“应该是可见的”。在实际工作中我越来越感受到并发编程并不是一个单独的知识点而是一条贯穿系统设计、代码编写、问题排查的完整脉络。它不是让自己写出复杂的逻辑来炫技而是让代码在极端场景下依然稳定、可靠。每处理完一个并发问题我都会把排查过程和数据记录下来哪怕是一两句话也有价值。下一次再遇到同样的问题时能省下大量时间。这篇笔记是我自己对并发编程的一次系统性回顾如果你愿意也可以把自己的并发案例写成笔记慢慢你会发现里面的规律是高度相似的——从基础工具的使用到架构层面的分工再到故障现场的定海神针全都离不开对底层机制的理解和对工程细节的敬畏。希望我踩过的这些坑能帮你在自己的项目里少走几步弯路。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

开源模型端侧落地实战:量化、推理加速与Agent上下文管理 2026/9/28 23:59:38

开源模型端侧落地实战:量化、推理加速与Agent上下文管理

1. 从"追平"到"端侧落地":开源模型这波到底变了什么如果你最近半年一直在关注模型圈的动态,应该能明显感觉到一个拐点:开源模型和闭源旗舰之间的差距,正在从"代差"变成"身位差"。以前大家…

阅读更多 →
Java采购管理系统实战:从数据库设计到事务一致性 2026/9/28 23:59:25

Java采购管理系统实战:从数据库设计到事务一致性

简介:这是一套面向Java Web初学者与课程设计者的采购管理系统完整源码,采用JSP技术搭建,配合MySQL数据库,用于解决企业采购信息的管理问题,适合作为毕业设计、课程大作业或进销存类项目的参考模板。系统实现了用户登录…

阅读更多 →
AI Evals实战指南:从零搭建LLM应用评估体系与CI/CD集成 2026/9/28 23:59:25

AI Evals实战指南:从零搭建LLM应用评估体系与CI/CD集成

1. 为什么AI Evals值得你花时间搞明白做LLM应用的人,迟早会撞上同一堵墙:模型输出飘忽不定,今天答得好好的,明天换个问法就胡说八道。你改了一版提示词,感觉好像好了点,但到底好了多少?说不清。…

阅读更多 →
LSTM时间序列预测实战:从数据窗口构造到模型调参避坑 2026/9/28 23:59:18

LSTM时间序列预测实战:从数据窗口构造到模型调参避坑

简介:这份资源面向高校学生与Python初学者,提供一套可直接运行的LSTM时间序列预测完整项目,适用于期末大作业、课程设计及入门级深度学习实践。项目以空气质量等真实数据为样本,覆盖数据预处理、模型搭建、训练与预测全流程&#…

阅读更多 →
LSTM时间序列预测实战:从期末大作业到可复现Python源码 2026/9/28 23:59:12

LSTM时间序列预测实战:从期末大作业到可复现Python源码

简介:这份资源面向高校学生与Python初学者,提供一套可直接运行的LSTM时间序列预测完整项目,适用于期末大作业、课程设计或入门深度学习实践。项目以空气质量等真实序列数据为样本,覆盖数据读取、预处理、模型搭建、训练与预测全流…

阅读更多 →
LLM红队实战:从攻击面枚举到防护策略的完整方法论 2026/9/28 23:59:12

LLM红队实战:从攻击面枚举到防护策略的完整方法论

1. 从“Lysios”这个名字说起:LLM红队到底在防什么第一次看到“Lysios – LLM red teaming org”这个标题,很多人会愣一下:Lysios是什么?是一个开源工具、一个组织代号,还是一套方法论?从命名习惯来看&…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉