Java并发编程应从理解线程安全问题入手,如竞态条件、可见性、JMM、happens-before规则;需亲手写bug代码并分析字节码;掌握调试手段与JDK版本差异。

Java并发编程不是从 Thread 开始学的,而是从“你已经写过带状态的多线程代码但出过 bug”开始的。没接触过线程安全、竞态条件、可见性问题的人,直接看 synchronized 或 java.util.concurrent 会像在读天书。
你得先会写并能看懂带共享变量的多线程代码
不是指“会调 start()”,而是能写出类似下面这种明显有问题的代码,并意识到它为什么错:
public class Counter {
private int count = 0;
public void increment() {
count++; // 非原子操作:读-改-写
}
public int getCount() { return count; }
}
常见错误现象:启动 10 个线程各调 1000 次 increment(),最后 getCount() 返回远小于 10000。
- 必须理解
count++在字节码层面是三条指令(getfield→iadd→putfield) - 要清楚 JVM 栈和堆的分工:每个线程有自己栈,但对象实例在堆上共享
- 不依赖 IDE 调试器,能用
javap -c Counter看字节码验证理解
JVM 内存模型(JMM)不是选修,是必过门槛
很多并发 bug 的根源不在代码逻辑,而在你默认“变量改了另一线程马上看到”——这在 JMM 下不成立。
立即学习“Java免费学习笔记(深入)”;
关键点不是背定义,而是能判断哪些场景需要 volatile、哪些不能靠它解决:
-
volatile能禁止重排序 + 保证可见性,但不保证原子性(volatile int x; x++依然线程不安全) - 构造器内泄露
this引用会导致其他线程看到未初始化完成的对象(即使字段是final,没正确发布也不行) -
happens-before规则中,synchronized块的解锁操作 happens-before 后续同锁的加锁操作——这是理解锁释放/获取语义的基础
熟悉基础工具类前,先亲手实现一个线程安全容器
别急着用 ConcurrentHashMap,先试着给 ArrayList 加锁封装成线程安全版,再对比:
- 用
synchronized方法 vssynchronized(this)块 —— 锁粒度差异直接影响吞吐量 - 把
size()和get(int)都同步,是否意味着迭代时不会抛ConcurrentModificationException?答案是否定的,因为 fail-fast 是检测 modCount 变化,不是靠锁保护 - 尝试用
ReentrantLock替换synchronized,注意必须配对调用lock()/unlock(),且unlock()要放在finally块里
调试手段比语法更重要
并发问题往往偶发、难复现。光会写不行,得会抓证据:
- 用
jstack抓线程快照,识别死锁(输出里会有deadlock提示)或长时间阻塞在Object.wait() -
Thread.getState()返回WAITING/TIMED_WAITING/BLOCKED的区别要一眼能分清 - 用
-XX:+UnlockDiagnosticVMOptions -XX:+PrintConcurrentLocks让 JVM 输出当前持有锁的线程和等待队列 - 不要只信日志:
System.out.println()本身是同步的,可能掩盖竞态;改用java.util.logging或异步日志框架
最常被忽略的一点:JDK 版本差异极大。比如 ConcurrentHashMap 在 JDK 7 是分段锁,JDK 8 改为 CAS + synchronized,JDK 9+ 又引入了更激进的优化。看源码或文档时,务必确认你查的是对应版本的实现。











