Java并发编程核心知识有哪些,零基础如何快速掌握并发编程?
Java并发编程的核心就三件事:搞懂JMM的可见性与有序性规则、吃透线程池的参数与拒绝策略、学会用jstack和Arthas去定位真实的线上问题。其余的知识点,都是挂在这三根主干上的枝叶。这篇文章来自我自己的技术笔记,包含学习路径、踩坑记录和几段可复制的排查命令,都是Practice过的。
先建立心智模型:并发问题的根子在哪
很多人一上来就背volatile、背CAS,结果面试一问“为什么加了锁还是出错”就卡壳。问题出在跳过了底层模型。
三个词概括所有并发Bug
- 01可见性
一个线程改了共享变量,另一个线程看不见。CPU缓存、编译器优化都会造成这种结果。
- 02原子性
i++看着是一行,实际是读、加、写三步,中间可能被切走。
- 03有序性
编译器和处理器会重排指令,单线程下没问题,多线程下顺序就乱了。
Java内存模型(JMM)就是给这三种问题定规矩的。据Oracle官方Java语言规范文档,happens-before是判断数据竞争是否存在的唯一标准。日常用得最多的几条:程序顺序规则、监视器锁规则、volatile变量规则、线程启动与终止规则、传递性。记住一句话就够了——两个操作之间存在happens-before关系,前一个的结果对后一个可见。
volatile只解决可见性和禁止重排,不保证原子性。这一点我在早期项目里栽过跟头:用volatile修饰一个计数器,压测时数字总是偏小。后来换成AtomicInteger才正常。CAS本身也有坑,ABA问题要靠AtomicStampedReference加版本号解决。
Java并发编程学习路线怎么规划
我自己走过弯路,也带过两个新人,整理出一条比较省时间的顺序。
第一阶段:把基础原语吃透(约两周)
- 线程的创建方式与生命周期状态,重点是
WAITING和BLOCKED的区别。 - synchronized的锁升级过程:偏向锁、轻量级锁、重量级锁。JDK 15之后偏向锁已被默认禁用,这一点要跟上版本。
- volatile的语义与内存屏障。
- wait/notify与Condition的配合使用,注意wait必须放在while循环里判断条件。
第二阶段:JUC工具包(约三周)
先啃AQS,它是ReentrantLock、Semaphore、CountDownLatch的共同底座。AQS的核心是一个volatile的state变量加一个CLH变体的双向队列。看懂acquire和release两个方法,后面一大片类都能串起来。
然后是容器部分:ConcurrentHashMap的分段演进(JDK 8之后改成CAS+synchronized锁单个桶)、CopyOnWriteArrayList的读写分离思路、BlockingQueue的几种实现差异。
synchronized和ReentrantLock有什么区别
这是面试里出现频率极高的一组对比,也是实际选型时的真实纠结点。
| 维度 | synchronized | ReentrantLock |
|---|---|---|
| 实现层面 | JVM内置关键字,由monitorenter/monitorexit指令实现 | JDK层基于AQS实现 |
| 锁释放 | 代码块结束或异常自动释放 | 必须在finally里手动unlock |
| 公平性 | 非公平 | 可指定公平或非公平 |
| 高级功能 | 不支持 | 支持tryLock超时、可中断、多条件Condition |
| 性能 | JDK 6之后优化明显,轻量场景差距很小 | 高竞争场景下可控性更强 |
行业共识认为,没有特殊需求优先用synchronized,代码更短也更不容易漏释放。只有在需要超时放弃、可中断或者多条件等待时,才换成ReentrantLock。
线程池:线上事故的高发地带
并发这块真正在生产环境出问题的,多数不是锁,而是线程池。我参与过的一次接口超时排查,根因就是一个用了无界队列的线程池,任务堆到几十万,GC直接飙红。
Java线程池核心参数怎么设置
ThreadPoolExecutor的七个参数里,真正需要动脑的是这三个:核心线程数、最大线程数、任务队列。
任务提交后的流转顺序是固定的:先看核心线程是否满 → 再尝试入队 → 队列满了才扩到最大线程数 → 都满了走拒绝策略。很多人误以为线程数会一路涨到maximum,实际上队列一开大,最大线程数几乎永远用不上。
几个实操建议:
- 队列别用
Executors默认的LinkedBlockingQueue无界版本,容量写死,比如几百到一千,逼着线程池扩容或触发拒绝策略。 - CPU密集型任务,线程数参考CPU核数加一;IO密集型可以放大到核数的两倍以上,具体按QPS乘以平均耗时来估。
- 拒绝策略不要用默认的AbortPolicy还不捕获异常,至少自定义一个,记录日志并落库,或者用CallerRunsPolicy做背压。
- 线程工厂里给线程起有意义的名字,出问题时jstack一眼能认出是哪个池。
另外,Spring项目里别直接@Async就完事,最好自己声明一个ThreadPoolTaskExecutor的Bean,把参数显式写出来。
我在项目里踩过的坑与排查方法
工具这块,我常用的组合是jstack加Arthas,够用且轻量。
死锁排查,先拿PID,再执行:
jstack -l 12345 > thread_dump.txt
grep -A 20 "Found one Java-level deadlock" thread_dump.txt
CPU飙高的排查路径:top -Hp 12345找到占CPU最高的线程,把十进制线程号转十六进制,再去jstack输出里搜nid=0x...。
Arthas更顺手一些,thread -n 3直接看最忙的三个线程栈,thread -b能自动找出阻塞其他线程的那一个。
还有三个我亲历的坑,值得记一下:
- SimpleDateFormat共享导致的错乱。改成
DateTimeFormatter,它是不可变且线程安全的。 - 双重检查锁没加volatile。对象引用赋值和初始化可能重排,别的线程会拿到半成品对象。
- 在synchronized里做远程调用。锁的持有时间被IO拉长,线程池很快被打满。锁内只做内存操作,这是条硬规矩。
Java并发编程面试高频考点,以及我在地域上的观察
去年我在成都面了几家Java后端岗位,聊下来感受挺明确:并发这块问得不深,但问得很实。一线大厂爱追问AQS源码和AQS队列的入队细节,本地中型公司更关心“你怎么给一个接口配线程池”“线上CPU打满了你怎么查”。所以准备的时候,原理和排查手法得两头都顾上。
被问到的题大致归为四类:
- JMM与volatile语义、happens-before规则
- 锁的升级过程、synchronized与Lock的选择
- 线程池参数、拒绝策略、如何监控
- ConcurrentHashMap的并发安全实现,以及CAS与ABA
业内专家指出,面试官更在意的往往不是你能不能背出源码,而是有没有真实的排查经历。所以平时遇到超时、卡顿,别急着重启,先存一份线程栈,这些都是面试时最好的素材。
Java并发编程Q&A:三个被问最多的问题
volatile能替代锁吗?
不能。它只保证可见性和禁止指令重排,复合操作依然不是原子的。像count++这种读改写,必须靠锁或原子类。适用于状态标志位、一次性安全发布这类单写多读的场景。
线程池的线程数到底设多少合适?
没有通用答案,得看任务类型和机器规格。经验公式是:CPU密集型取核数加一,IO密集型取核数乘以(1加等待时间与计算时间的比值)。上线前做一轮压测,观察平均响应时间和队列积压,比任何公式都准。设完之后还要配监控,队列长度和活跃线程数这两个指标建议打到监控面板上。
面试问到并发,只背八股够不够?
不够。八股能帮你过初筛,但深挖一层就会露底。比较稳的做法是手里有两三个真实案例,能讲清楚现象、用jstack或Arthas看到的证据、最后怎么改的代码,以及改完之后压测数据的变化。这类回答在成都的Java后端面试里,通过率明显高于纯背概念。