
Java 从诞生之初就将多线程支持内置在语言中——synchronized关键字、Object.wait()/notify()、Thread类都是 JDK 1.0 就存在的。但这种“内置”并不意味着简单。并发编程的复杂性来自硬件、操作系统和 JVM 三个层面的叠加理解每一层如何相互作用才可能写出正确且高效的多线程代码。一、Java 内存模型并发编程的契约Java 内存模型定义了一套规则规范了多线程环境下共享变量的访问行为。它不是描述内存的物理布局而是描述什么情况下一个线程对共享变量的修改对另一个线程可见。可见性问题来源于现代 CPU 的多级缓存结构。CPU 核读写变量时数据可能停留在 L1/L2/L3 缓存中尚未写回主存。另一个核从主存读取同样的变量看到的是旧值。这不是 Java 的问题这是计算机体系结构的问题。JMM 通过定义 happens-before 规则来解决可见性。happens-before 关系是判断两个操作是否有序的依据如果操作 A happens-before 操作 B那么 A 的结果对 B 可见。关键规则包括程序次序规则单线程中按代码顺序执行的操作存在 happens-before 关系volatile 变量规则对 volatile 变量的写入 happens-before 后续对该变量的读取锁规则解锁操作 happens-before 后续对该锁的加锁操作传递性A happens-before BB happens-before C则 A happens-before C原子性问题涉及不可分割的操作。i不是原子操作——它包含读取、修改、写入三个步骤多个线程同时执行时可能丢失更新。JMM 保证对long和double之外的读写操作是原子的但复合操作必须使用同步机制来保证原子性。有序性问题源于编译器和 CPU 对指令的重排序。只要不改变单线程的执行结果编译器和 CPU 可以自由重排指令以提高性能。但在多线程环境中重排序可能导致意料之外的执行顺序。volatile和synchronized都包含内存屏障防止重排序跨越临界区。JMM 的实质不提供绝对的内存一致性而是提供一个可靠性边界——使用正确的同步机制程序就能在任意平台上得到预期的行为不使用则没有任何保证。二、volatile轻量级同步机制volatile是最轻量的同步机制它保证可见性和有序性但不保证原子性。写入 volatile 变量会强制将值刷新到主存随后其他线程读取该变量时会从主存获取最新值。编译器不会将 volatile 变量的操作重排序到内存屏障之外。这些特性使 volatile 适合作为状态标记如volatile boolean running true;控制循环终止。但 volatile 无法保证复合操作的原子性。典型的错误用法是volatile int counter;然后counter——递增操作依然存在竞态条件。需要原子递增时应使用AtomicInteger。三、synchronized从偏向锁到重量锁的演进synchronized是 Java 最基础的同步原语但其底层实现在 JDK 演进过程中经历了多次重构。早期的synchronized是重量级锁每次加锁都会切换到内核态性能开销极大。从 JDK 1.6 开始引入了锁升级机制偏向锁锁默认偏向第一个获得它的线程。当锁被一个线程反复获取时偏向锁只需检查 Mark Word 中的线程 ID无需 CAS 操作。偏向锁通过取消偏向来竞争释放。轻量级锁当第二个线程尝试获取偏向锁时锁升级为轻量级锁。线程通过 CAS 竞争锁自旋等待。轻量级锁不涉及内核态切换适合锁竞争不激烈的场景。重量级锁自旋超过一定次数后锁升级为重量级锁。线程被阻塞等待操作系统调度唤醒。重量级锁涉及用户态到内核态的切换开销较大。锁只能升级不能降级这是为了减少取消偏向的开销。锁升级的阈值可以通过-XX:BiasedLockingStartupDelay和-XX:PreBlockSpin调整。锁的膨胀过程与对象头中的 Mark Word 紧密相关——对象头存储了锁的状态标志和指向锁记录的指针。理解锁升级需要理解对象头中的状态位在不同场景下如何变化。四、CAS无锁并发的基础CAS 是一条 CPU 原子指令比较并交换从内存读取值 V与预期值 A 比较相等则将内存值更新为 B否则返回失败。Java 通过Unsafe类提供compareAndSwapObject、compareAndSwapInt和compareAndSwapLong等方法暴露 CAS。AtomicInteger.incrementAndGet()的实现在循环中调用 CAS直到成功为止。这种自旋 CAS 在高并发场景下可能导致频繁的重试和 CPU 开销。CAS 的 ABA 问题内存值从 A 变为 B 再变回 ACAS 认为没有变化但实际上发生了变化。AtomicStampedReference通过版本号解决 ABA 问题。实际工程中 ABA 问题的破坏性取决于应用场景不是所有场景都需要处理。五、AQS同步器的基础框架AbstractQueuedSynchronizer是java.util.concurrent包的核心ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock都基于 AQS 实现。AQS 的核心是一个volatile int state和一个等待队列。子类通过tryAcquire和tryRelease等钩子方法控制 state 的获取和释放逻辑。独占模式如ReentrantLockstate 为 0 表示锁空闲成功获取锁时 state 加 1释放时减 1。重入时 state 多次递增。共享模式如Semaphorestate 表示剩余许可数每次获取减 1释放加 1。等待队列是一个 CLH 队列Craig, Landin, and Hagersten 队列的变体通过自旋和LockSupport.park()实现线程阻塞与唤醒。ReentrantLock与synchronized的选择synchronized更简洁锁升级机制让它在大多数场景性能足够ReentrantLock提供更多功能——可中断锁、超时锁、公平锁、多条件变量。这些功能不是日常所需但在特定场景下不可替代。六、并发容器与工具ConcurrentHashMap是java.util.concurrent包中最重要的容器。在 JDK 1.7 中采用分段锁将数据分为多个 Segment每个 Segment 独立加锁。JDK 1.8 改为 CAS synchronized插入时先 CAS 尝试失败再使用 synchronized 锁定目标 Node。这种设计让ConcurrentHashMap的并发度远高于Hashtable或Collections.synchronizedMap。CopyOnWriteArrayList在写操作时复制整个底层数组。读操作不需要锁但写操作开销大且数据有延迟。适合读多写少的场景如配置列表的读取。BlockingQueue接口的实现如ArrayBlockingQueue、LinkedBlockingQueue用于生产者消费者模式提供阻塞的put和take操作。CountDownLatch与CyclicBarrier用于线程协调。前者使一个或多个线程等待直到一组操作完成后者让一组线程互相等待到达共同屏障点后继续执行。七、实际开发中的陷阱this引用逸出在构造函数中启动线程或将this传递给外部可能导致对象尚未完全初始化就被其他线程访问。修复方法是使用静态工厂方法在构造完成后才暴露对象引用。双重检查锁定的经典写法在 JDK 1.5 之前是失效的因为new操作可能重排序导致其他线程看到未初始化的对象。使用volatile修饰实例变量可以修复此问题但更好的方式是使用静态内部类单例或Enum实现。ThreadLocal内存泄漏ThreadLocalMap的 key 使用弱引用但 value 是强引用。线程池中的线程长时间存活ThreadLocal 的 value 可能在不再使用后仍然被引用。解决方案是在finally块中调用ThreadLocal.remove()。八、性能调优建议锁竞争导致的上下文切换成本远高于 CAS 自旋。判断锁竞争程度用jstack查看线程状态大量线程处于BLOCKED状态说明锁竞争严重。应对措施包括缩小临界区范围、使用ReentrantLock的tryLock减少阻塞、用ConcurrentHashMap替代同步 Map、用读写锁分离读写的互斥。volatile的写入操作会触发内存屏障但读操作没有同步开销。适当地在状态标记中使用 volatile可以避免不必要的锁开销。九、小结Java 的并发模型建立在 JMM、synchronized 锁升级机制和 AQS 框架之上。理解 JMM 的 happens-before 规则是正确性基础了解synchronized的锁升级路径才能评估锁的性能开销掌握 AQS 才能灵活使用 JUC 工具类。并发不是独立于其他知识模块的“附加特性”——它贯穿于 JVM 的每一个角落是 Java 语言区别于其他系统级语言的核心特性之一。