java 并发编程有哪些核心概念

接入与开发

把 Java 并发核心概念和 Python 等语言对照讲清,给出能直接落地的排查与验证顺序,不背概念。

写 Java 的人一搜并发,满屏都是锁、线程池、内存模型,真到项目里又不知道先看哪个。这篇不堆术语,按「概念对照 + 排查顺序」的方式把核心概念串一遍,并顺带对上 Python 那套说法,方便跨语言转过来的同学少走弯路。看完你应该能自己判断一段并发代码该从哪里入手看。

写 Java 的人一搜并发,满屏都是锁、线程池、内存模型,真到项目里又不知道先看哪个。这篇不堆术语,按「概念对照 + 排查顺序」的方式把核心概念串一遍,并顺带对上 Python 那套说法,方便跨语言转过来的同学少走弯路。看完你应该能自己判断一段并发代码该从哪里入手看,而不是背完定义还是不敢改。

先说清楚定位:这是一份判断标准和排查顺序,不是语法手册。里面的每条准则都有它不适用的场景,我会尽量写出来。如果你想要的是各家中转站或官方接口在并发调用上的表现差异,那属于另一类问题,可以到站内的工具目录和价格追踪页面核对当时的记录,本文只讲代码层面的事。

先分清三组常被混用的词

很多人讨论并发问题时吵起来,根源是两个人说的根本不是同一件事。先把三组词掰开,后面所有判断才有共同基础。这三组词决定了你该选什么工具,也决定了你该盯什么指标。

并发
任务在调度上交替推进,同一时刻未必真的在同时跑,重点是「看起来同时在处理很多事」
并行
多个任务在同一时刻真的同时执行,通常要求有多核或独立的执行单元
串行
一件事做完再做下一件,没有交错,最容易推理也最容易成为瓶颈
同步/异步
说的是「要不要等结果」:同步是调用方一直等到有结果,异步是先拿到一个凭证、结果稍后交付
阻塞/非阻塞
说的是「等的时候线程能不能干别的」:阻塞时线程被占住,非阻塞时线程可以先去处理别的任务
GIL
Python 解释器层面的一把全局锁,使得同一进程内多个线程无法真正并行执行字节码

最容易混的是同步/异步和阻塞/非阻塞这两组。它们是两个独立维度,组合起来有四种情况,别用一个词概括。判断准则其实很简单:任务里大部分时间在等 IO,就偏并发模型,用少量线程撑住大量请求;任务里大部分时间在算 CPU,就偏并行模型,得靠多核分担。

这条准则不适用的情况是任务本身非常短,短到线程调度和上下文切换的开销盖过了并发带来的收益。这种时候老老实实串行跑,往往比硬上线程池更快也更稳。Python 那边还多一层:因为 GIL,多线程更偏并发,想真并行通常得走多进程,代价是进程间数据要序列化传递。

原子性、可见性、有序性:Java 并发的三个根问题

Java 里的并发 bug,绝大多数最后都能归到这三件事上。记住这三个词不是为了考试,是为了排查时有个固定的提问顺序,不至于东看西看。

问题一句话解释Java 里靠什么保证Python 里的对应情况
原子性一段操作不可被切开,中间不会被别的线程插进来锁、原子类锁;`+=` 这类复合操作同样不是原子的
可见性一个线程改了值,另一个线程能不能及时看到volatile、synchronizedGIL 让单条字节码级的问题少一些,但复合操作照样会丢更新
有序性编译器和 CPU 可能重排指令,执行顺序和写的顺序不一致volatile 划出边界没有对应的显式关键字,靠锁和队列把顺序固定住

排查顺序建议固定成三步:先问有没有共享可变状态,再问这份状态有没有被正确保护,最后才考虑要不要换并发模型。顺序反了会很痛苦——很多人一上来就想换框架、换模型,结果发现根因只是某个字段忘了做保护。

  • 共享可变状态是万恶之源,能做成不可变或每线程一份,就别共享
  • 能被多个线程读写的字段,逐个过一遍它是被锁保护、被 volatile 标记,还是压根没保护
  • `i++` 这种看着像一条语句的写法,实际上读改写三步,两边语言里都不是原子的
  • 单例、缓存、计数器、连接池这几类对象是并发问题的重灾区,优先审它们
  • 如果一段逻辑只在单线程里跑,就别为了「以后可能并发」提前加锁,锁会掩盖真正的结构问题

线程池:Java 这边最该先搞懂的一块

线程池是 Java 并发里性价比最高的知识点:搞懂了能避开一大类线上问题,投入也不大。核心参数就那么几个,真正容易出事的只有队列选型和拒绝策略这两处。

队列用无界队列时,最大线程数这个参数基本失效——任务进不来新线程,全堆在队列里,表现是「不报错,但越来越慢,内存还一直涨」。这种坑在压测时未必暴露,因为压测时间短,队列还没堆满。拒绝策略也别用默认值,默认那个会直接抛异常,业务侧得想清楚到底是丢任务、丢最老的,还是让提交方自己跑。

  1. 先确定任务类型任务是 CPU 密集还是 IO 密集?CPU 密集的线程数别超过核数太多,IO 密集可以适当放大。这个判断没有标准答案,要靠压测找拐点。
  2. 再定队列有界队列能逼出压力,让系统在过载时明确拒绝;无界队列会吞掉压力,直到内存或延迟崩掉。生产环境优先有界。
  3. 显式指定拒绝策略别依赖默认。想清楚过载时业务能接受什么:丢新任务、丢老任务、还是让调用线程同步执行来自然限流。
  4. 验证回收观察空闲回收时间设得合不合理。线程建了不回收,数量会一直挂在高位,这类问题在流量回落之后才看得出来。
  5. 对上 PythonPython 对应的是线程池和进程池两套执行器,思路一样,区别在于用线程还是用进程,以及有没有 GIL 这道坎。

锁与无锁:什么时候该换工具

这一节只讲一件事:换工具的时机。很多人一遇到锁竞争就想上无锁方案,顺序反了,结果是把简单问题做成了复杂问题。

先用最简单的互斥锁把逻辑写对,确认确实存在性能问题,再考虑优化。反过来做的,基本都是在给自己挖坑:无锁代码难写、难测、难排查,收益还不一定为正。无锁不是没有锁,是把竞争交给 CAS 重试,适合冲突少、操作简单的场景;冲突一多,大量线程反复重试,反而比直接排队加锁更慢。

现象可能原因先查什么
结果偶尔错共享状态没保护,或可见性问题临界区是否完整覆盖了读写,有没有字段漏加保护
吞吐上不去锁竞争,或临界区里有耗时操作把 IO 和外部调用挪出锁外,再看锁粒度
程序卡住不响应死锁,或线程被长时间占住两个锁的获取顺序是否一致,锁内是否调了外部方法
线程/进程数持续上涨任务没正常结束,或池子没回收任务标识和线程名对不对得上,有没有任务卡在等待

死锁排查有固定套路,按顺序过一遍能覆盖大部分情况:先看有没有两个锁的获取顺序不一致,再看锁住的区间里是不是调了外部方法(包括回调、日志、远程调用)。不适用的情况也有:如果临界区里本来就有 IO 或耗时调用,正确做法是先把它挪出锁外,而不是去换一种锁。

并发优化里,把耗时操作挪出临界区,收益通常比换锁方案大一个量级。

内存模型与运行时:Java 独有、Python 没有的那部分

这是跨语言迁移时最容易踩空的一块。Java 的内存模型规定了线程之间通过主内存交换数据的规则,volatile、synchronized、final 各在这个规则里占一个位置。Python 没有这套显式规范,它的并发正确性更多依赖 GIL 和标准库给的对象语义。

关键在于:这套保证在跨进程时会变。一旦数据要经过序列化传给另一个进程,原来靠共享内存享受的语义就没了,你得自己重新确认数据完整性和顺序。所以从 Python 转 Java,最该补的是可见性和指令重排这两块;从 Java 转 Python,最该补的是「什么时候必须上多进程」。

  • 共享状态怎么传:是共享内存、消息队列,还是每次拷贝一份,三种方式对正确性的要求完全不同
  • 生命周期谁管:谁创建、谁释放、异常时谁来收尾,这个问题在两边都容易被忽略
  • 异常怎么被观察到:子线程里抛的异常默认可能被吞掉,必须有地方兜住并上报
  • 顺序有没有保证:跨进程、跨队列之后,原来依赖的先后关系未必还成立
  • 别把 Java 的写法直译过去:final 字段的语义、volatile 的作用,在 Python 里都没有一一对应的东西

上线之后:并发问题的排查顺序

线上出并发问题,最忌讳一上来就翻代码。先看现象落在哪,能省掉大量时间。三种现象——结果错、慢、线程或进程数一直涨——对应的排查方向完全不同。

  1. 先分类现象是结果错了,还是变慢了,还是执行单元的数量一直涨?先归类,别混着查。
  2. 结果错走原子性和可见性重点看共享字段有没有被完整保护,锁的范围够不够,有没有半途被人改了值。
  3. 慢就走队列和锁竞争看队列有没有积压、临界区里有没有耗时操作、锁的粒度是不是太粗。
  4. 数量涨就走生命周期看线程池有没有正常回收、任务有没有卡在等待、有没有任务创建后没被正确结束。
  5. 日志要能对上至少记线程名或任务标识,以及进入和离开临界区的时间点。没有这个,并发问题基本没法复盘。
  6. 用拐点代替理论值把并发数逐步往上加,观察从哪个点开始退化。这个拐点比任何教科书上的数字都有参考意义。

Java 和 Python 的并发概念对照表怎么用

跨语言读代码时,最怕的是把两边的概念硬套。对照表的作用是让你看到熟悉的名字时知道对方在说什么,而不是让你照搬实现——两边的运行时假设本来就不一样。

概念Java 里的说法Python 里的说法注意点
执行单元线程线程 / 进程Python 多线程受 GIL 限制,真并行走多进程
互斥synchronized、显式锁锁对象、上下文管理器锁的语义两边不完全一致,别直译
可见性volatile无直接对应Python 靠 GIL 和对象语义,跨进程后保证更弱
池化执行线程池线程池 / 进程池执行器参数含义接近,但队列和拒绝策略的默认行为不同

真正会跨语言的人,是先确定并发模型,再挑语言里对应的工具,而不是先挑了工具再倒推模型。如果你只是临时在项目里加一段并发逻辑,最省事的做法是照着已有代码的写法来——保持一致性比引入新范式重要得多。

另外提醒一句:不同语言、不同运行时、不同部署环境下,同一种并发模型的实际表现差异可能很大,尤其是在网关或转发层。这部分不属于语言概念,属于选型和环境问题。如果你在挑具体的接口服务或转发方案,最好按当时的实测记录来判断,不要凭概念表直接下结论,可以到站内相关页面核对当时的数字。

下一步概念对照看完了,接下来该看真实环境的并发表现。站内的测速与工具目录里有按场景分类的验证方式,配合价格追踪页面一起看,能帮你判断某个方案在你的并发模型下到底扛不扛得住,而不是只看宣传页上的说法。

常见问题

Java 并发和 Python 并发最大的区别在哪?

主要在运行时假设上。Java 是真正的多线程并行,共享状态的可见性和重排要你自己管;Python 有 GIL,多线程更适合 IO 密集,要真并行一般走多进程。所以同一套并发思路,落到两个语言里的默认做法和踩坑点并不一样。

线程池的参数到底怎么定?

没有通用答案,但有个顺序:先按任务性质分 IO 密集还是 CPU 密集,IO 密集可以多给线程,CPU 密集一般贴着核数。然后看队列会不会积压、拒绝策略触发时业务能不能接受。最后压一轮,按实际拐点回调参数,比照抄别人的配置靠谱。

volatile 是不是就能保证线程安全?

不是。它主要解决可见性和一定的有序性,但复合操作比如自增,它的原子性并不由 volatile 保证。要保证这种操作不出错,还是得用锁或原子类。很多人第一次踩坑就是以为加了 volatile 就万事大吉。

怎么判断一段代码有没有并发问题?

按三个问题过一遍:有没有共享可变状态,这份状态有没有被保护,保护的范围是不是刚好包住临界区。三条里有一条答不上来,就大概率有问题。另外别忘了子线程里的异常如果没被观察,会表现为「什么都没发生但结果不对」。

并发数往上调就能更快吗?

不一定。到某个点之后,锁竞争、上下文切换、下游限流都会吃掉收益,曲线会变平甚至往下走。正确做法是逐步加压找拐点,并且同时看错误率和延迟,别只看吞吐。具体在哪个点退化,跟你的业务和下游能力强相关。

本文依据各平台公开文档与官方公告整理,只做中文归纳;具体做法、参数与限制以官方文档为准。