news 2026/9/29 18:10:36

Java并发与并行实战:从线程模型到分布式锁的核心技术解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java并发与并行实战:从线程模型到分布式锁的核心技术解析

1. 并行与并发,别再傻傻分不清

做Java开发的兄弟,不管你是刚入行的新人,还是已经写了三五年的老手,面试中大概率都遇到过这个问题:并行和并发到底有什么区别?我之前带过不少实习生,很多人张口就来“并发就是同时执行多个任务”,这个回答其实只说对了一半。实际上并发和并行是两个维度完全不同的概念,背后对应的是不同的硬件资源利用方式和程序设计思路。

先说结论:并发(Concurrency)关注的是任务的交错执行,核心在于“调度”;并行(Parallelism)关注的则是任务的真正同时执行,核心在于“多核”。你的电脑只有一个CPU核的时候,照样可以并发跑几千个线程,但它们并不是同时在执行,而是CPU在不停地切换上下文,让每个线程都分到一点时间片。只有当你的机器上有多个CPU核,多个线程才能被分配到不同的核上真正同时执行,这才叫并行。

这个区别不是咬文嚼字,而是直接决定了你怎么设计系统。如果你的服务部署在单核机器上,那再怎么开线程也不会提升吞吐量,反而会因为上下文切换开销增加延迟;如果你手上有16核32G的服务器,写并发程序却不考虑并行度,线程全挤在几个核上排队,那这颗服务器的性能就被白白浪费了。所以看清并发与并行的本质,是学好Java并发编程的第一课,也是后面所有高并发方案设计的基础。

这篇文章我结合自己多年做Java后端、搞过高并发IM、做过并发仿真压测的实际经验,把并发与并行这条线上的核心知识点串起来讲一遍,从JVM线程模型、锁机制、JUC工具,到线程池调优和分布式场景下的并发方案,全程带实操代码和踩坑记录。适合正在准备Java面试的人,也适合要在真实项目中把并发性能做上去的开发者。

2. Java线程模型与并发并行落地的底层原理

2.1 JVM线程与操作系统线程的映射关系

要理解Java并发,首先得知道Java线程到底跑在什么上面。JVM里的Thread对象,本质上是对操作系统线程的一层包装。以前在JDK 1.2之前Java用的是绿色线程(Green Thread),也就是JVM自己模拟多线程,不依赖操作系统,后来因为调度效率和多核利用问题被彻底放弃了。现在的HotSpot VM里,每一个Thread对象背后基本就是一个原生操作系统线程(1:1映射),线程的创建、调度、阻塞、唤醒都由操作系统内核完成。

这带来一个重要推论:Java线程不是越开越好,因为每个线程都要占用独立的栈空间(默认Xss是1MB),线程切换时要保存和恢复寄存器状态、程序计数器等信息,这些上下文切换开销是实打实的。我做压测的时候观察过,线程数从100涨到1000,吞吐量往往不是线性增长,反而在某个临界点之后开始回落,原因就是CPU时间大量耗费在线程切换上,而不是真正在处理业务。

涉及操作系统层面的并发调度,有个关键指标叫上下文切换(Context Switch)。并发场景下线程切换太频繁,CPU的有效利用率就会下降。想要降低上下文切换,通用手段有三个:减少锁的持有时间、使用无锁数据结构(比如AtomicLong、ConcurrentHashMap)、降低线程数量。这三个点都会在后面的实践环节展开。

2.2 并发三要素:原子性、可见性、有序性

聊Java并发,绕不开并发编程的三座大山:原子性、可见性、有序性。不懂这三点,后面学锁和并发工具类就是空中楼阁。

原子性是指一个操作或者多个操作要么全部执行且不被任何因素打断,要么全部不执行。典型的例子就是i++,看起来是一句代码,实际上在字节码层面是“读取-修改-写入”三步操作,两个线程同时执行就可能互相覆盖。保证原子性的方式就是加锁,或者使用AtomicInteger等CAS原子类。

可见性是指当一个线程修改了共享变量时,其他线程能够立刻看到这个修改。这里有个经典陷阱:很多人在多线程环境用普通boolean变量做开关,在主线程修改flag,子线程却一直读不到变化。原因是每个线程在工作内存中拷贝了一份变量副本,JIT编译器甚至会把变量优化到寄存器里,导致主内存的修改没被感知。解决方法是使用volatile修饰变量,当然synchronized和Lock也能保证可见性。

有序性是指程序执行的顺序按照代码的先后顺序执行。JVM为了性能会做指令重排,这在单线程下不影响结果,但在多线程下可能导致诡异的逻辑错误。你需要明白volatile还有一个作用就是禁止指令重排,这正是单例模式双重检查锁(DCL)里为什么一定要用volatile修饰instance字段的原因。很多年经验的开发者,能在面试中把DCL为什么加volatile讲透,基本并发基础就过关了。

2.3 并行度与硬件资源的关系

并行度这个概念,说白了就是系统同一时刻真正有几个任务在同时执行。它受两个因素制约:物理CPU核心数和任务的拆分方式。一个16核的服务器,理论上并行度上限就是16个线程同时在跑(假设没有超线程),超过这个数量,多出来的线程就得排队等待被调度。

在实际项目里,并行度通常不等于线程池大小。因为线程在执行过程中会经常阻塞(等待IO、等待数据库返回、等待网络响应),阻塞期间CPU核心是空闲的,可以调度其他线程来用。所以对于IO密集型的应用,线程池大小应该设置得比核心数大很多,常见公式是线程数 = CPU核心数 * (1 + IO等待时间 / CPU计算时间)。而对于CPU密集型的任务,也就是纯计算不打IO,线程数设置为核心数或者核心数+1就够了,设太多反而增加切换开销。

我在压测一个IM消息转发服务的时候遇到过一件印象深刻的事:代码里每个请求进来都开一个新线程去处理,结果4核8G的机器,压到200并发就开始频繁Full GC和线程创建异常。后面改成线程池固定8线程,性能反而提升了快5倍。这个案例我后面还会详细讲。

3. Java并发基础实操:锁、CAS与并发容器

3.1 synchronized的底层原理与锁升级路径

synchronized是Java内置的同步关键字,每个用过的人都会写,但能把它底层原理讲明白的人不多。在JDK 1.6之前,synchronized是重量级锁,直接依赖操作系统互斥量(Mutex)实现,一加锁就陷入内核态,性能很差。JDK 1.6之后引入了锁升级机制,让synchronized的启动成本大幅降低。

锁升级的完整路径是无锁态到偏向锁,然后是轻量级锁,争抢激烈再膨胀为重量级锁。偏向锁的意思是JVM认为这个锁大概率只有一个线程访问,就在对象头里记录这个线程的ID,之后该线程再次进入同步块不需要任何CAS操作。如果出现第二个线程竞争,偏向锁撤销,升级为轻量级锁,其实就是自旋锁,通过CAS不断尝试获取锁,不进入阻塞状态。自旋到一定次数还拿不到锁,就升级为重量级锁,让线程进入内核态阻塞。

这里有个很实用的经验:在低竞争场景下,synchronized并不比ReentrantLock差,代码还更简洁。在高竞争场景下,ReentrantLock由于支持公平锁、可中断、可超时等特性,更灵活可控。性能上两者并没有绝对碾压,真正的瓶颈还是在临界区代码的执行时间上。所以选哪个,我建议优先问自己是否需要高级特性,而不是网上那些“性能对比”帖子。

3.2 CAS机制与ABA问题

CAS全称是Compare And Swap,比较并交换。它的核心逻辑是一条CPU原子指令:先比较内存中的值是否等于预期值,如果等于就把新值写入,否则操作失败。Java里的AtomicInteger、LongAdder等原子类都是基于CAS实现的,ConcurrentHashMap在JDK 8的实现里也大量使用了CAS。

CAS的优点是轻量,不阻塞线程,失败就重试,适合竞争不激烈的场景。缺点有两个,一是高竞争下自旋会白白消耗CPU,二是有经典的ABA问题。ABA问题指的是:线程A读到值是1,线程B把值改成2又改回1,线程A再次CAS时发现值还是1,认为没有变化就写入了新值,但实际上中间的值被改变过。解决ABA问题的方法是用带版本号的原子引用AtomicStampedReference。

我在实际开发中遇到的多数ABA场景其实没什么破坏性,但在做并发队列或者栈的时候,务必考虑版本号。举个例子,用CAS实现一个无锁栈,线程A准备弹出栈顶节点N,线程B此刻把N出栈,又做了入栈操作让N重新成为栈顶,这时候线程A的CAS可能成功弹出已经被引用的旧节点,造成逻辑错乱。这类问题极其隐蔽,线上出现需要排查很久。

3.3 ConcurrentHashMap的演进与使用要点

ConcurrentHashMap几乎是Java并发编程中使用率最高的容器。JDK 7时代的实现是分段锁(Segment),每一段是一个独立的Hash表,各自可以并发读写,整体并发度等于Segment数量。JDK 8开始放弃分段锁,改成了CAS + synchronized锁桶(bin)的方式,锁粒度更细,性能更好。

从实践角度,ConcurrentHashMap有几个使用要注意的细节。第一,size()方法在并发下是一个估算值,不是一个精确并发快照,如果你需要精确统计,得额外加锁或使用LongAdder维护计数器。第二,虽然它是线程安全的,但“先检查后操作”这种复合操作依然需要自己加锁,比如经典的if (!map.containsKey(key)) { map.put(key, value); }有两个线程同时走到中间,还是会把数据覆盖。正确做法是用putIfAbsent或者compute这些原子方法。

第三点是我特别想提醒的,ConcurrentHashMap不允许null键和null值,这跟HashMap不同。原因是作者Doug Lea认为并发下无法区分“没有这个key”和“这个key的值是null”,干脆直接不允许。新手经常在这里踩坑,往里面放null值直接抛NPE。

4. JUC核心工具类详解:AQS、CountDownLatch与信号量

4.1 AQS框架:Java并发锁的基石

AQS全称是AbstractQueuedSynchronizer,抽象队列同步器,是ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock这些工具共用的底层框架。理解了AQS,JUC并发工具就等于看懂了一大半。

AQS的核心是一个volatile int state(同步状态)和一个双向等待队列。以ReentrantLock为例,state表示锁的重入次数,线程获取锁成功就CAS把state加1,失败就封装成Node节点加入等待队列尾部,然后通过LockSupport.park阻塞自己。释放锁时state减1,如果减到0说明锁完全释放,就唤醒队首节点。

AQS支持两种模式:独占模式和共享模式。ReentrantLock是独占模式,同一时间只有一个线程能持有锁。CountDownLatch和Semaphore是共享模式,多个线程可以同时通过。面试时候能被问到“AQS的state在Semaphore里代表什么”,答案就是剩余的许可数量。

这里有个源码阅读的建议:不要硬啃AQS的全部实现,先把acquire和release两套流程走通,再去看ReentrantLock的公平/非公平区别,最后看Semaphore和CountDownLatch如何使用共享模式,这样梯度学习效率最高。

4.2 CountDownLatch与CyclicBarrier:等待多个线程完成

CountDownLatch是倒计数锁存器,典型使用场景是主线程拆一个任务给N个子线程并行处理,然后主线程等待所有子线程完成再继续。使用方式就是初始化new CountDownLatch(N),每个子线程在处理结束后调用countDown(),主线程调用await()阻塞等待计数归零。

这里面有一个常见的坑:如果某个子线程抛异常挂了,countDown()没被调用,主线程会一直阻塞。所以在生产环境必须用try/finally确保countDown()一定会执行,或者在await上设置超时时间,比如await(30, TimeUnit.SECONDS)。我见过不止一个事故是子线程异常导致主流程卡死,最后用超时兜底解决的。

CyclicBarrier和CountDownLatch看起来像,但语义完全不同。CountDownLatch是一次性的,计数归零后就不可再用了;CyclicBarrier是循环屏障,所有线程到达屏障点后一起放行,然后可以重复使用。打个生活化的比方:CountDownLatch像一车乘客等人全到齐了司机才发车,CyclicBarrier像跑步比赛,所有选手都准备好后在发令枪响时同时起跑,而且可以连续跑很多轮。

4.3 Semaphore限流:控制并发访问量

Semaphore信号量的作用是对共享资源做访问许可控制。比如一个接口同时最多允许50个线程访问,那就可以初始化new Semaphore(50),每个请求进来先acquire()获取许可,处理完释放许可。对于控制并发数、限流场景非常实用。

和高并发IM的推送可能会联想到一起,当系统同时给大量客户端推送消息时,如果不做信号量控制,很容易把IO线程池打满。我当时给推送服务配置了Semaphore限制同时进行的推送任务数为核心数的两倍,请求过来如果不能立即获取许可,就直接返回“稍后重试”的反压信号,系统稳了很多。

要注意Semaphore的公平性问题。非公平信号量吞吐高,但可能出现线程饥饿;公平信号量按队列顺序发放许可,适合对延迟敏感的场景。构造参数new Semaphore(permits, fair)的第二个参数就是公平开关,生产环境建议根据对延迟波动的容忍度来选择。

5. 线程池设计与仿真压测实践

5.1 ThreadPoolExecutor核心参数与任务流程

线程池是Java并发编程中必须熟练掌握的工具。ThreadPoolExecutor有7个核心参数:核心线程数(corePoolSize)、最大线程数(maximumPoolSize)、空闲保活时间(keepAliveTime)、时间单位(unit)、工作队列(workQueue)、线程工厂(threadFactory)、拒绝策略(handler)。

任务提交后的完整流转过程是这样的:首先判断当前线程数是否小于核心线程数,如果是则创建新线程执行任务;否则尝试把任务放入工作队列;如果队列满了,再判断当前线程数是否小于最大线程数,如果是则创建新线程执行任务;如果已经达到最大线程数,则走拒绝策略。

拒绝策略有四种:AbortPolicy直接抛异常(默认)、CallerRunsPolicy由提交任务的线程自己执行、DiscardPolicy静默丢弃、DiscardOldestPolicy丢弃最老的未处理任务。我做IM系统时选的是CallerRunsPolicy,因为直接抛异常会让请求失败,而由调用方线程执行可以在流量过大时天然起到降速作用,虽然响应变慢,但系统不会被压垮。

一个常见的误解是核心线程数和最大线程数设成一样大就完事了,这其实放弃了线程池弹性扩容的能力。在有突发流量但大部分时候低负载的场景,核心线程数设小一些、最大线程数设大一些,配合有界队列,能兼顾资源占用和突发吞吐。

5.2 线程池调优:从公式到实测

线程池大小设多少没有银弹,但有两条常用的基础公式。CPU密集型任务:核心线程数 = CPU核心数 + 1,多一个线程是为了在个别线程因缺页中断或暂停时顶上。IO密集型任务:核心线程数 = CPU核心数 * (1 + 平均IO等待时间 / 平均CPU计算时间),这个公式的本质是当线程阻塞等待IO时,CPU可以调度其他线程干活,因此线程数可以远大于核心数。

公式只能给起点,真正的调优还得靠压测。我自己的调优流程是:

  1. 先根据任务类型选公式,估一个初始值
  2. 用JMeter或者自研并发仿真程序,逐步增加并发线程数,记录QPS、RT、CPU使用率
  3. 观察CPU使用率在哪个线程数下接近85%左右且QPS不再明显增长,那个点就是线程池规模的甜点区
  4. 继续加大并发数观察RT是否指数级飙升,如果RT快速恶化,说明线程池已经过载,需要回退参数

继续压到2000并发时,RT突然从10ms飙升到800ms。后面我发现原因是数据库连接池被打满,线程大量阻塞等待数据库连接,这个瓶颈就不在线程池本身了。所以调优时一定要分层观察:Web层线程池、业务线程池、数据库连接池、消息队列消费者的线程池,每一层都要知道自己的水位,排查时才能快速定位瓶颈在哪一层。

5.3 工作队列选型:有界与无界的博弈

工作队列是线程池设计中容易被忽略但极其关键的点。LinkedBlockingQueue默认是无界的,这意味着任务永远能进队列,线程池永远不会触发拒绝策略和最大线程数扩容,同时队列里的任务无限堆积,内存不断增长,最终OOM。我接手过一个内存溢出的老项目,排查下来就是消息处理用了无界队列,高峰时段任务积压上千万条,直接把堆内存打爆。

实务上我基本只用有界队列,比如ArrayBlockingQueue,容量根据业务可接受的积压量来控制。比如每个任务平均处理耗时100ms,能容忍的最大延迟5秒,那队列容量大约就是50。队列满之后配合拒绝策略做降级处理。

这里还要推荐一个冷门但很贴近并行的工具:CompletionService。它内部封装了线程池和阻塞队列,提交批量任务后,谁先完成就先拿到谁的结果,而不是按提交顺序阻塞等待。我在做大整数并行加法拆分、批量图片处理这类需要汇总多个子任务结果的场景时,用CompletionService比手动维护Future列表要优雅得多。

6. 分布式场景下的并发与并行方案

6.1 分布式锁:从数据库锁到Redis锁再到ZooKeeper

单机场景可以用synchronized和ReentrantLock解决互斥问题,一旦服务部署成多节点集群,本地锁就失效了,这时候需要分布式锁。常见的实现路径有基于数据库、基于Redis、基于ZooKeeper和基于etcd。

数据库锁最简单直接,用唯一索引或者SELECT ... FOR UPDATE就能实现,缺点是性能差,数据库会成为瓶颈。Redis分布式锁是目前最主流的方案,核心是SET key value NX PX timeout一条原子命令,设置成功就说明获取到了锁,PX设置过期时间防止客户端挂了锁不释放。但标准Redis锁有一个“锁过期但任务还没执行完”的问题,另一个常见问题是主从切换时锁可能丢失。

Redisson库给Redis锁做了一套比较成熟的封装:可重入锁、红锁、看门狗自动续期。看门狗机制会在锁过期前自动续期,默认每10秒续一次,这样即使任务执行超过锁的过期时间,锁也不会被其他节点抢走,除非整个服务宕机。这解决的正是我上面说的“误删锁”场景。

ZooKeeper分布式锁基于临时顺序节点实现,优点是可靠性高,客户端断开连接自动释放锁,不会出现Redis那种锁丢失问题;缺点是集群成本高,性能比Redis差一个量级。一般情况下业务锁用Redis就足够了,需要强一致的场景再考虑ZK。

6.2 消息队列削峰:并发写请求的缓冲机制

高并发场景下,直接把所有请求交给后端处理,系统很容易被打垮。消息队列的削峰填谷本质是一种异步解耦的并行策略:请求先快速写入MQ,消费者按自己的能力从MQ拉取消息处理,这样即使瞬时流量到达峰值10倍于系统处理能力,MQ也能把压力缓冲掉。

这里要敲黑板强调一个消费者侧的并发配置:@KafkaListener或者RocketMQ的PushConsumer,每个消费者实例的并发线程数必须根据下游处理能力和队列积压情况严格控制。消费者线程数也不是越多越好,如果下游数据库写入能力有限,消费者线程开再多也只是把压力堆在数据库连接池上,导致数据库雪崩。

我用RocketMQ时习惯是:每个消费组并行消费线程数控制为下游数据库可承受的连接数上限之内,并且监控消费延迟和积压量,积压超过阈值就告警而不是动态扩容。动态扩容听起来很美好,但在下游资源有限时毫无意义,扩容消费者只会加速下游故障。

6.3 并行计算框架:从Fork/Join到虚拟线程

单机并行计算方面,Java原生提供了Fork/Join框架,JDK 8的并行流parallelStream()底层就是用它实现的。Fork/Join的核心思想是分治:把大任务拆成足够小的子任务,子任务并行执行,最后合并结果。适合大整数加法并行、数组排序、矩阵计算这类可以被拆分成互相独立子任务的计算类型。

大整数加法本来是一个串行逐位相加的问题,但拆分成多段并行相加,最后再合并进位,可以让多核CPU真正跑起来,这也对应了标题里“并行”的关键词。不过要注意,任务拆分的粒度太小时,Fork/Join的任务创建和调度开销可能超过并行收益,我实验下来单段数据量少于一定阈值时,并行反而不如串行。

JDK 21正式带来了虚拟线程(Virtual Threads),这是Java并发模型的一个大变革。虚拟线程的线程数可以开到几十万甚至上百万,因为它不直接映射操作系统线程,而是由JVM调度挂在少数平台线程上。对于IO密集型的Web应用,虚拟线程可以极大简化并发编程,不需要再绞尽脑汁设计异步回调,直接用同步代码也能扛高并发。我们内部跑过对比测试,Spring Boot + 虚拟线程模式下,单机支撑并发连接数比传统线程池模式翻了接近一个数量级,而且代码几乎没改。

7. 并发环境下的数据一致性与锁优化实践

7.1 单机锁与分布式锁的组合使用

实际业务中,单机锁和分布式锁不是二选一,而是配合使用。以一条比较经典的“库存扣减”场景为例:多个应用节点的用户同时下单,先通过Redis分布式锁保证同一个商品只允许一个节点进入扣减流程;进入流程后,节点内部再用本地锁或数据库行锁保证该节点内多个线程不做重复扣减。

之所以要做两层,是因为分布式锁有网络开销,不能对每个内部操作都加一遍。组合方案的优点是减少分布式锁的持有时间:拿到分布式锁后,只需完成必要的预校验和生成一个幂等令牌,然后释放分布式锁,具体扣减操作在本地通过数据库事务和行锁完成。把分布式锁只在最关键的竞争入口处短时间持有,能显著提升整体吞吐量。

7.2 数据库并发锁与隔离级别

数据库并发控制是后端开发绕不开的硬骨头。脏读、不可重复读、幻读这三种问题,对应的事务隔离级别各有不同。MySQL默认的隔离级别是REPEATABLE READ(可重复读),它通过MVCC多版本并发控制来保证一致性读,通过间隙锁和临键锁来防止幻读。

实际开发和面试里最常被问到的是行锁和间隙锁的区别。行锁(Record Lock)锁的是索引记录本身,间隙锁(Gap Lock)锁的是索引记录之间的间隙,防止其他事务在这个间隙插入新记录。间隙锁是幻读的防御手段,但也是死锁高发的原因之一。一个典型的死锁场景是:事务A先锁定一个范围的记录,事务B也锁定另一个范围的记录,然后互相尝试锁定对方的范围,于是一起死锁。

排查死锁最直接的方法是执行SHOW ENGINE INNODB STATUS查看最近一次死锁的日志,里面会清楚显示两个事务持有和等待的锁资源。我们团队内部对抢购活动做过复盘,发现很多死锁是不同请求操作同一批商品时,加锁顺序不一致引发的。解决办法很简单:所有访问共享资源的代码,严格按同一顺序加锁,这个习惯养成了,单机数据库死锁能少一大半。

7.3 乐观锁与悲观锁的选型逻辑

乐观锁和悲观锁的选择,本质上是对冲突概率的判断。悲观锁认为冲突一定会发生,所以先加锁再读数据,数据库实现是SELECT ... FOR UPDATE,Java实现是synchronized。乐观锁认为冲突很少发生,所以不加锁,在更新时用版本号或CAS校验,数据库实现是UPDATE ... SET version = version + 1 WHERE version = ?,Java实现是AtomicInteger。

乐观锁适合读多写少、冲突概率低的场景,因为不加锁并发度更高;悲观锁适合写多写少的场景,否则乐观锁会因为大量更新失败重试,反而更慢。我做过一个积分系统的优化,原先写积分明细用悲观锁,并发一高就排队严重,后来改成版本号乐观锁,冲突重试三次,整体吞吐提升了一倍多。重试也要控制次数,无限重试在高冲突下会放大数据库压力。

8. 常见并发问题排查与高性能设计总结

8.1 线上并发问题排查工具箱

真到了线上环境,靠猜是解决不了并发问题的,得靠工具和数据说话。我这里列几个我每次排查并发问题都会用到的命令和工具,纯干货,建议直接收藏:

  • jps:查看Java进程PID,这个不用多说,定位进程是第一件事
  • jstack <pid>:打印线程堆栈,查看是否有线程卡在某个锁上、是否有死锁,这个命令是排查线程问题的头号工具,我基本每天都会用
  • jstat -gcutil <pid>:查看GC情况,频繁Full GC会严重影响并发吞吐
  • jmap -dump:导出堆内存快照,配合MAT分析内存泄漏
  • top -H -p <pid>:查看进程内每个线程的CPU占用,发现某个线程CPU爆高,用jstack找出对应线程ID的堆栈
  • Arthas:阿里开源诊断工具,在线反编译、方法执行监控、动态改日志级别,功能强大。排查线上偶发并发问题时,我经常用watch命令观察某个方法的入参和返回值

处理并发问题的标准节奏应该是:先用监控确定现象(吞吐下降/超时/OOM),再用线程转储和堆转储定位可疑线程和对象,最后结合代码锁区和数据量做假设,验证再修改。千万不要一上来就看代码,没有数据支撑的猜测大概率是乱蒙。

8.2 并发场景下写不好就炸的坑位清单

并发编程最可怕的是问题不一定稳定复现,有时候压测跑两个小时不出错,上线一周炸一次,让人头大。根据我这些年的经验,下面这些坑是重灾区:

第一,双重检查锁(DCL)没有加volatile。这是并发面试必考,也是实际代码里高频踩坑的位置。不加volatile,指令重排可能导致拿到半初始化的对象。

第二,SimpleDateFormat线程不安全。它的内部使用了Calendar对象,多线程并发parse和format会抛NumberFormatException或者得到错乱结果。正确做法是使用ThreadLocal给每个线程一个副本,或者直接用Java 8的DateTimeFormatter(本身线程安全)。

第三,HashMap在多线程下扩容产生死循环。JDK 7的链表头插法在扩容时会导致环形链表,线程调用get时会陷入死循环。虽然JDK 8改成了尾插法解决了这个问题,但依然不推荐在多线程下使用HashMap,实在要用的场景优先ConcurrentHashMap。

第四,synchronized (String)锁字符串。字符串常量池的存在会让两个逻辑上不同的锁对象因为值相同互相锁住,而且字符串内容一旦改了就换了锁,非常容易出诡异问题。

第五,自增操作不原子。count++看起来无害,多线程累加结果一定不对。团队里如果有新人用它做计数,被坑的概率极高,我一般在代码评审里看到这种就直接打回。

8.3 高并发系统的分层并发设计思路

最后整理一套我在设计高并发系统时的分层思路,算是把前面所有知识点串联起来。

最上面的接入层,靠负载均衡把流量分散到多台机器,这是系统级的并行。Web容器层,配置合适的HTTP线程池,控制每个请求的并发处理能力。业务逻辑层,根据任务类型设计线程池,IO密集还是CPU密集区别对待,这个就是标题里“智能仿真并发”的核心——针对不同任务的特性做对应的并发仿真和压测。数据层,数据库连接池配置上限,SQL走索引,避免慢查询拖垮整个线程池;Redis做热点数据的缓冲和分布式锁的承载。最后是消息层,下游来不及处理的流量丢进MQ做削峰,消费者按下游能力并发消费。

这个分层设计里每一层都有对应的并发控制手段,崩溃也是层层缓冲,不会一下子把压力打到底层数据库。我们内部做16核32G服务器的压测目标,就是单机抗住几千并发连接,QPS过万,RT控制在100ms以内,这个目标通过合理的线程池设置加Redis缓存加MQ削峰是可以达到的。如果这几层都做对了,系统在高并发下的表现会非常稳定,而不是碰运气式的能扛不能扛。

并发与并行的核心不是去记工具类的API,而是理解每层设计背后的权衡:锁粒度大了性能差,小了可能不安全;线程池大了浪费内存,小了吞吐上不去;队列长了延迟高,短了拒绝多。我这些年做的并发项目,核心工作其实就是不断在这些维度之间找平衡点,找到之后系统自然就稳了。你可以把本篇文章里的公式和工具当成起点,然后在自己项目的压测环境里跑一跑,记录数据,慢慢就会形成自己的并发调优手感。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 18:10:28

Jetson Orin NX串口读取GNSS模块:NMEA解析与RTK定位实战

做室外机器人定位这件事&#xff0c;绕不开GNSS。我最近在Jetson Orin NX 16GB上接了一个联适R70M-GNSS模块&#xff0c;目标很明确&#xff1a;用串口把高精度定位数据读出来&#xff0c;并解析成程序能直接用的经纬度和状态信息。这套组合不是什么高深技术&#xff0c;但正因…

作者头像 李华
网站建设 2026/9/29 18:10:24

Ego-Planner实战:解决无人机撞树问题的避障指南

室外飞无人机&#xff0c;我最怕的不是炸机&#xff0c;是撞树。尤其多旋翼在树林、果园或者园区绿地里穿行&#xff0c;树冠、细枝、电线杆&#xff0c;随便来一下就是螺旋桨报废、电机堵转&#xff0c;运气差一点整机直接摔。很多朋友第一反应是“避障没做好”&#xff0c;但…

作者头像 李华
网站建设 2026/9/29 18:09:17

AUTOSAR诊断:从DTC到DEM,解析故障码生命周期与配置实践

那天售后反馈过来说客户那台车“发动机故障灯”亮着但不抖动也不跛行&#xff0c;诊断仪一读&#xff0c;出现一个历史故障码P0171。我盯着那个confirmed bit和failed since last clear bit同时存在的状态发了会儿呆——这个场景我见过太多次了。很多刚接触AUTOSAR诊断的同学会…

作者头像 李华
网站建设 2026/9/29 18:09:12

.NET 9 + Cursor实现5分钟稳定串口上位机开发

1. 为什么是“5分钟搞定”&#xff1f;——串口上位机开发的旧痛与新解过去三年&#xff0c;我带过七支工业软件小团队&#xff0c;从PLC数据采集到产线HMI定制&#xff0c;几乎每个项目都绕不开一个“小而重”的环节&#xff1a;串口上位机。它不复杂&#xff0c;但极其琐碎—…

作者头像 李华
网站建设 2026/9/29 18:07:55

英语感叹句深度解析:How Tom and Polly laughed句型结构与应用

1. “How Tom and Polly laughed!”&#xff1a;一句话定性的语法身份判断第一次看到这个句子的人&#xff0c;十有八九会愣一下&#xff1a;怎么以How开头&#xff0c;后面跟着的却是一个完整的“主语 谓语”结构&#xff1f;How不是“怎么、如何”的意思吗&#xff1f;难道这…

作者头像 李华
网站建设 2026/9/29 18:07:52

.NET 9.0 + VS Code 快速开发串口上位机

1. 项目概述&#xff1a;为什么串口上位机开发还在用VS Code和.NET 9.0&#xff1f;我做工业自动化软件开发快十二年了&#xff0c;从最早的VB6串口控件、Delphi串口组件&#xff0c;到后来C# WinForms、WPF&#xff0c;再到近几年的Blazor Web上位机&#xff0c;见过太多“看起…

作者头像 李华