学会Java并发编程需要哪些基础知识_学习前准备说明

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

Java并发编程不是从 Thread 开始学的,而是从“你已经写过带状态的多线程代码但出过 bug”开始的。没接触过线程安全、竞态条件、可见性问题的人,直接看 synchronizedjava.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++ 在字节码层面是三条指令(getfieldiaddputfield
  • 要清楚 JVM 栈和堆的分工:每个线程有自己栈,但对象实例在堆上共享
  • 不依赖 IDE 调试器,能用 javap -c Counter 看字节码验证理解

JVM 内存模型(JMM)不是选修,是必过门槛

很多并发 bug 的根源不在代码逻辑,而在你默认“变量改了另一线程马上看到”——这在 JMM 下不成立。

关键点不是背定义,而是能判断哪些场景需要 volatile、哪些不能靠它解决:

  • volatile 能禁止重排序 + 保证可见性,但不保证原子性(volatile int x; x++ 依然线程不安全)
  • 构造器内泄露 this 引用会导致其他线程看到未初始化完成的对象(即使字段是 final,没正确发布也不行)
  • happens-before 规则中,synchronized 块的解锁操作 happens-before 后续同锁的加锁操作——这是理解锁释放/获取语义的基础

熟悉基础工具类前,先亲手实现一个线程安全容器

别急着用 ConcurrentHashMap,先试着给 ArrayList 加锁封装成线程安全版,再对比:

  • synchronized 方法 vs synchronized(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+ 又引入了更激进的优化。看源码或文档时,务必确认你查的是对应版本的实现。