引言:一个永不停止的循环

作为一名Java开发者,你是否曾遇到过如下诡异的情景?这段代码在你的IDE里运行一段时间就会停止,但在生产环境的服务器上却可能永远停不下来。

public class NeverStop {
    private static /*volatile*/ boolean stop = false; // 注意:这里没有volatile

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (!stop) { // 子线程无法"看到"主线程修改后的值
                // 疯狂循环
            }
            System.out.println("Thread stopped.");
        }).start();

        Thread.sleep(1000);
        stop = true; // 主线程修改共享变量
        System.out.println("Main thread set stop to true.");
    }
}

问题根源并非Java的BUG,而是源于现代计算机的硬件架构。为了充分压榨CPU性能,计算机会采用多级缓存、指令乱序执行等优化策略。这导致了同一个变量在多个CPU核心的“视角”下可能拥有不同的值。

Java内存模型(Java Memory Model, JMM) 就是一个为了屏蔽各种硬件和操作系统的内存访问差异,让Java程序在各种平台下都能达到一致的内存访问效果而提出的抽象规范。它定义了线程如何与主内存进行交互,是理解Java并发编程的基石。

本文将围绕JMM,从基础概念到高级特性,并结合实战案例,带你彻底攻克并发编程中的可见性、原子性和有序性问题。

一、核心概念:理解JMM的抽象模型

1.1 主内存与工作内存

JMM的主要目标是定义程序中各个变量的访问规则。它规定了所有变量都存储在主内存中。

  • 主内存:存储所有共享变量。是线程共享的区域。
  • 工作内存:每个线程都有自己的工作内存,其中保存了该线程使用到的变量的主内存副本拷贝。线程对变量的所有操作(读、写)都必须在工作内存中进行,而不能直接读写主内存中的变量。

它们之间的交互关系,可以用下图清晰地表示:

sequenceDiagram
    participant MainMemory as 主内存
    participant ThreadA as 线程A工作内存
    participant ThreadB as 线程B工作内存

    Note over MainMemory, ThreadA: 8种原子操作保障内存交互
    
    rect rgb(240, 248, 255)
        Note over MainMemory, ThreadA: 读取操作流程
        ThreadA->>MainMemory: lock (锁定变量)
        MainMemory->>ThreadA: read (读取值)
        ThreadA->>ThreadA: load (载入工作内存)
        ThreadA->>ThreadA: use (使用变量)
    end
    
    rect rgb(255, 242, 232)
        Note over MainMemory, ThreadA: 写入操作流程
        ThreadA->>ThreadA: assign (赋值给工作变量)
        ThreadA->>MainMemory: store (存储到主内存)
        MainMemory->>MainMemory: write (写入主内存)
        MainMemory->>MainMemory: unlock (解锁变量)
    end
    
    Note over ThreadA, ThreadB: 线程间通信必须通过主内存
    ThreadA->>MainMemory: store/write (写入)
    MainMemory->>ThreadB: read/load (读取)

(JMM内存交互模型)

交互协议:JMM定义了8种原子操作来完成主内存与工作内存的交互:

操作名称作用范围功能描述
lock (锁定)主内存变量将变量标识为线程独占状态
unlock (解锁)主内存变量释放变量的锁定状态
read (读取)主内存变量将变量值从主内存传输到工作内存
load (载入)工作内存变量将read得到的值放入工作内存的变量副本中
use (使用)工作内存变量将工作内存变量值传递给执行引擎
assign (赋值)工作内存变量从执行引擎接收值,赋给工作内存变量
store (存储)工作内存变量将工作内存变量值传输到主内存
write (写入)主内存变量将store传输的值放入主内存变量中

JMM规定了这些操作的执行顺序和规则:

  • 不允许read和load、store和write操作单独出现
  • 不允许一个线程丢弃它最近的assign操作(变量在工作内存中改变了之后必须把该变化同步回主内存)
  • 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存
  • 一个新的变量只能在主内存中"诞生",不允许在工作内存中直接使用一个未被初始化(load或assign)的变量
  • 一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次
  • 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值
  • 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量
  • 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store和write操作)

注意readloadstorewrite必须成对出现,但不必连续。中间可以插入其他指令,这就为重排序提供了可能。

1.2 内存屏障

在深入JMM的解决方案之前,必须了解一个底层概念——内存屏障(Memory Barrier),也称内存栅栏(Memory Fence)。它是理解volatile如何工作的基石。

为什么需要内存屏障? 编译器和CPU为了优化性能会进行指令重排序。这在单线程下无懈可击,但在多线程下会导致灾难。内存屏障就像是一个“刹车”,告诉编译器和CPU:在此处,某些类型的重排序必须停止!

内存屏障主要分为四种类型,几乎所有的并发同步机制都间接或直接地使用了它们:

屏障类型指令示例说明
LoadLoadLoad1; LoadLoad; Load2确保Load1的数据装载先于Load2及其后所有装载指令。
StoreStoreStore1; StoreStore; Store2确保Store1的数据刷新到主内存先于Store2及其后所有存储指令。
LoadStoreLoad1; LoadStore; Store2确保Load1的数据装载先于Store2及其后所有存储指令。
StoreLoadStore1; StoreLoad; Load2确保Store1的数据刷新到主内存先于Load2及其后所有装载指令。这是一个“全能型”屏障,开销最大。

JMM的作用:它通过在不同的字节码指令(如monitorenter/monitorexitvolatile读写)前后插入特定类型的内存屏障,来禁止特定类型的重排序,从而为开发者提供清晰的内存可见性保证。

1.3 缓存一致性协议(MESI)

JMM 是一个抽象模型,而它的很多设计灵感与约束,都来自于底层的硬件机制,其中最重要的就是缓存一致性协议

为什么需要缓存一致性?

现代CPU的多级缓存架构极大地提升了性能,但也导致了“数据副本”不一致的问题。为了解决这个问题,硬件设计者提出了一系列缓存一致性协议,其中最著名的是 MESI 协议

MESI 协议工作原理

MESI 是四种缓存行(Cache Line)状态的缩写,任何时刻一个CPU核心中的缓存行都处于以下状态之一:

  • M (Modified - 修改) :缓存行是脏的(Dirty),即数据已被当前CPU核心修改,与主内存中的数据不一致。此缓存行拥有该数据的“独家所有权”。
  • E (Exclusive - 独占) :缓存行是干净的(Clean),数据与主内存一致,且只有当前CPU核心拥有这份副本。
  • S (Shared - 共享) :缓存行是干净的,数据与主内存一致,但可能存在于多个其他CPU核心的缓存中。
  • I (Invalid - 无效) :缓存行中的数据是无效的,不能使用。下次访问时必须从主内存或其他CPU缓存中重新加载。

CPU核心之间通过一个复杂的消息传递机制来维护这些状态,从而保证所有缓存中的数据副本最终保持一致。其交互过程可以通过以下状态机来表示:

stateDiagram-v2
    direction LR
    [*] --> I : 初始状态
    I --> E: CPU发起写请求<br>缓存行不存在
    I --> S: CPU发起读请求<br>其他缓存有或无(S状态)
    I --> M: CPU发起写请求<br>缓存行不存在

    E --> M: 本地CPU写
    E --> S: 其他CPU读请求<br>状态变为共享(S)
    M --> S: 其他CPU读请求<br>先写回内存再共享
    M --> I: 其他CPU写请求<br>先写回内存再失效
    S --> I: 其他CPU写请求<br>收到失效信号
    S --> M: 本地CPU写<br>发送失效信号给其他缓存

    note right of I
      初始或已被丢弃
      必须从主内存读取
    end note

工作流程举例(简化的读/写交互):

会议室白板比喻:我们可以把主内存比作公司服务器上的共享云文档,把每个CPU的缓存比作每个员工工位上的个人白板

MESI协议的工作流程,就像一套员工之间协作更新白板和云文档的默契规则:

sequenceDiagram
    participant CPU_A
    participant CPU_B
    participant Main Memory as 主内存 (云文档)
    Note over CPU_A, CPU_B: 初始状态: 云文档 X=0

    CPU_A ->> Main Memory: 读取 X (Read)
    activate CPU_A
    Main Memory -->> CPU_A: 返回 X=0
    Note right of CPU_A: 我的白板: X=0 (状态:独占-E)
    deactivate CPU_A

    CPU_B ->> Main Memory: 读取 X (Read)
    activate CPU_B
    Main Memory -->> CPU_B: 返回 X=0
    Note left of CPU_B: 我的白板: X=0 (状态:共享-S)
    CPU_A -->> CPU_B: 嘿,我也有一份副本!
    Note right of CPU_A: 状态降级为共享-S
    deactivate CPU_B

    Note over CPU_A, Main Memory: CPU_A 要修改 X=1
    CPU_A ->> CPU_B: 发送“无效化”(Invalidate)消息
    Note left of CPU_B: 收到!擦掉我白板上的X (状态:无效-I)
    CPU_B -->> CPU_A: 确认无效化 (Ack)
    CPU_A ->> CPU_A: 在自己的白板上修改 X=1
    Note right of CPU_A: 状态升级为修改-M

    CPU_B ->> Main Memory: 再次读取 X (Read)
    activate CPU_B
    CPU_A ->> Main Memory: 监测到请求,先将白板上的 X=1 <br>写回云文档 (Write-Back)
    Note right of CPU_A: 状态降级为共享-S
    Main Memory -->> CPU_B: 返回最新的 X=1
    Note left of CPU_B: 我的白板: X=1 (状态:共享-S)
    deactivate CPU_B

第一步:独占读取 (Read for Exclusive)

  • CPU-A 需要读数据X。它发现自己的缓存里没有(I状态),于是向总线发起请求。
  • 它从主内存读到值0,并记录在自己的缓存中。此时其他核心都没这个数据,所以CPU-A独占这份数据,状态变为E

第二步:共享读取 (Read for Share)

  • CPU-B 也需要读X。它发起请求。
  • CPU-A 的缓存控制器嗅探到了这个请求,将其缓存行状态从E改为S,并允许数据被共享。
  • CPU-B 从主内存(或通过缓存间传输)读取数据,并标记为S状态。

第三步:写入修改 (Write to Modified)

  • CPU-A 现在要修改X,将其设为1
  • 由于状态是S(共享),它不能直接修改,必须先获得独占权
  • CPU-A 向总线发送一个无效化消息,告诉所有其他缓存了X的副本(CPU-B):“我要改了,你们的副本马上作废!”
  • CPU-B 收到消息后,将自己的缓存行状态置为I(无效),并回复一个确认消息。
  • CPU-A 收到所有确认后,才能放心地在自己的缓存中修改X=1,并将状态升级为M(修改)。此时,数据只存在于CPU-A的缓存中,且与主内存不一致

第四步:同步与再次共享

  • CPU-B 后来也需要读X。它发现自己的副本是I(无效),于是发起请求。
  • CPU-A 的缓存控制器嗅探到了这个请求。它知道自己持有最新的脏数据(状态M),于是拦住这个发向主内存的请求。
  • CPU-A 做两件事:1) 将自己缓存中的最新值(X=1写回主内存;2) 将数据传给CPU-B。
  • 之后,CPU-A 和 CPU-B 的缓存行状态都变为S(共享),数据再次保持一致。

现在我们可以看到,MESI 协议已经在硬件层面解决了缓存一致性问题,那为什么还需要 JMM 和 volatile 呢?

原因在于 性能。CPU核心间发送“无效化”消息和等待响应是需要时间的(CPU 时钟周期)。如果CPU-A 在等待CPU-B 确认“无效化”的这段时间里什么都做不了,那将是对计算资源的巨大浪费。

为了解决这个性能问题,CPU 设计引入了 Store Buffer 和 Invalidate Queue

  • Store Buffer:CPU 要写入数据时,如果发现缓存行不是E或M状态(即不独占),它会把写入操作放到一个专用的 存储缓冲区(Store Buffer)  中,然后异步地发送“无效化”消息并继续执行后续指令,而不会傻傻地等待响应。这导致了指令重排序
  • Invalidate Queue:其他CPU核心收到“无效化”消息后,并不会立即处理,而是将其放入一个 无效化队列(Invalidate Queue)  中,并立即回复“已收到”,然后再择机去真正失效掉自己缓存中的副本。这导致了可见性延迟

正是这些硬件优化(Store Buffer, Invalidate Queue)的引入,导致了即使有MESI协议,依然会出现可见性和有序性问题。

而 JMM 的作用,就是通过在不同程序点(如 volatile 写、 释放)插入内存屏障,来告诉CPU:“在执行到这的时候,必须清空Store Buffer,处理Invalidate Queue,保证所有的读写操作都符合MESI协议的规定流程,不允许搞任何小动作(重排序和延迟失效)”,从而绕过这些异步优化带来的副作用,为程序员提供确定性的内存可见性保证。

1.4 并发三大问题:原子性、可见性、有序性

1. 原子性(Atomicity)

原子性是指一个或多个操作要么全部执行成功,要么全部不执行,不会被打断。

案例i++ 操作不是原子性的。它实际上分为三步:

  1. 从主内存read变量i到工作内存。
  2. 在工作内存执行i+1操作(use/assign)。
  3. 将新值store/write回主内存。

如果两个线程同时执行i++,可能会出现交错执行,导致最终结果小于预期。

public class AtomicityDemo {
    private int count = 0;

    public void increment() {
        count++; // 非原子操作
    }
}

2. 可见性(Visibility)

可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。

案例:本文开头的例子就是典型的可见性问题。线程1修改了stop的值,但未能及时刷回主内存,或者线程2未能及时从主内存更新自己的副本,导致线程2无法“看见”这个变化。

3. 有序性(Ordering)

有序性是指程序执行的顺序按照代码的先后顺序执行。但为了性能,编译器和处理器会进行指令重排序

案例:双重检查锁定(DCL)单例模式。

public class Singleton {
    private static Singleton instance; // 错误写法,缺少volatile

    public static Singleton getInstance() {
        if (instance == null) { // 第一次检查
            synchronized (Singleton.class) {
                if (instance == null) { // 第二次检查
                    instance = new Singleton(); // 问题所在!
                }
            }
        }
        return instance;
    }
}

instance = new Singleton(); 这行代码分为三步:

  1. 分配对象内存空间
  2. 初始化对象(调用构造函数)
  3. 将instance引用指向内存地址

步骤2和3可能被重排序。如果线程A执行完1和3后被挂起,此时instance已非null但对象未初始化。线程B在第一次检查时看到instance非null,会直接返回一个未初始化完成的对象,导致程序错误。

解决方案:使用 volatile 禁止重排序。

private static volatile Singleton instance; // 正确写法

二、深入JMM解决方案:Happens-Before与volatile

2.1 Happens-Before原则

Happens-Before是JMM最核心的概念。它是一组规则,用于判断两个操作之间的内存可见性。如果操作A Happens-Before 操作B,那么A所做的任何更改对B都是可见的。

重要原则

  1. 程序次序规则:一个线程内,书写在前面的操作Happens-Before后面的操作。
  2. 监视器锁规则:对一个锁的解锁Happens-Before于后续对这个锁的加锁。
  3. volatile变量规则:对一个volatile变量的写Happens-Before于任意后续对这个volatile变量的读。
  4. 传递性:如果A Happens-Before B,且B Happens-Before C,那么A Happens-Before C。
  5. start()规则Thread.start()的调用Happens-Before于启动线程中的任何操作。
  6. join()规则:线程中的所有操作Happens-Before于其他线程成功从该线程的join()中返回。

Happens-Before关系并不等同于时间上的先后,它强调的是内存可见性的保证

2.2 volatile 关键字:轻量级的同步

volatile是JMM提供的轻量级同步机制。它通过底层插入内存屏障来实现两大特性:

  1. 保证可见性:当一个线程修改了volatile变量,新值会立即被强制刷到主内存。并且其他线程的工作内存中该变量的副本会立即失效,迫使它们必须从主内存重新读取最新值。
  2. 禁止指令重排序:通过插入内存屏障来禁止编译器和处理器的优化重排序。
sequenceDiagram
    participant MainMemory as 主内存 (volatile变量v)
    participant ThreadA as 线程A工作内存
    participant ThreadB as 线程B工作内存

    Note over MainMemory: volatile变量保证可见性
    
    ThreadA->>MainMemory: read v
    MainMemory->>ThreadA: load v
    ThreadA->>ThreadA: use v
    
    Note over ThreadA: 线程A修改v
    ThreadA->>ThreadA: assign v (新值)
    ThreadA->>MainMemory: store v (立即刷新到主内存)
    MainMemory->>MainMemory: write v
    
    Note over MainMemory: 立即通知其他线程
    MainMemory->>ThreadB: 使ThreadB的工作内存中v失效
    ThreadB->>MainMemory: read v (重新读取最新值)
    MainMemory->>ThreadB: load v (新值)

其内存屏障策略如下图所示,这直接对应了 volatile变量规则

flowchart LR
    subgraph VolatileWrite [Volatile 写操作]
        direction LR
        A[之前的写操作] --> B[StoreStore Barrier]
        B --> C[Volatile Write]
        C --> D[StoreLoad Barrier]
    end

    subgraph VolatileRead [Volatile 读操作]
        direction LR
        E[Volatile Read] --> F[LoadLoad Barrier]
        F --> G[LoadStore Barrier]
        G --> H[后续的读/写操作]
    end
  • StoreStore屏障:禁止上面的普通写和下面的volatile写重排序。
  • StoreLoad屏障:禁止上面的volatile写和下面可能有的volatile读/写重排序。(开销最大)
  • LoadLoad屏障:禁止下面的普通读和上面的volatile读重排序。
  • LoadStore屏障:禁止下面的普通写和上面的volatile读重排序。

volatile的应用:DCL单例模式

public class Singleton {
    // 使用volatile关键字防止指令重排序
    private static volatile Singleton instance;
    
    // 私有构造函数防止外部实例化
    private Singleton() {
        // 初始化代码
    }
    
    /**
     * 获取单例实例的公共方法
     * 使用双重检查锁定来减少同步开销
     */
    public static Singleton getInstance() {
        // 第一次检查(无锁)
        if (instance == null) {
            // 同步代码块
            synchronized (Singleton.class) {
                // 第二次检查(有锁)
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

在DCL单例中,volatile关键字至关重要,因为它解决了以下问题:

  1. 防止指令重排序instance = new Singleton()这行代码实际上包含三个步骤:

    • 分配对象内存空间
    • 初始化对象(调用构造函数)
    • 将instance引用指向内存地址

    如果没有volatile,步骤2和3可能被重排序,导致其他线程看到一个非null但未完全初始化的对象。

  2. 保证可见性:确保一个线程对instance的修改能立即对其他线程可见。

重要提示volatile不保证原子性。对于复合操作(如count++),仍需使用synchronizedAtomicInteger等原子类。

2.3 synchronized 关键字:全能的同步

synchronized 关键字是 JMM 提供的、功能最强大的同步原语之一。它直接对应着 JMM 中的 lockunlock 操作,能够完整地解决并发三大问题,其底层实现同样依赖于内存屏障。

它如何解决三大问题?

  1. 原子性 (Atomicity)

    • synchronized 块或方法确保了一次只有一个线程可以执行被保护的代码段。这使得像 i++ 这样的复合操作在同步块内成为原子操作。
  2. 可见性 (Visibility)

    • 根据 监视器锁规则:对一个锁的解锁 Happens-Before 于后续每一个对这个锁的加锁。
    • 具体机制:当线程释放锁(执行monitorexit指令)时,JMM 会强制将该线程的工作内存中的修改刷新到主内存中。当线程获取锁(执行monitorenter指令)时,JMM 会使该线程的工作内存中的变量副本失效,从而必须从主内存重新加载共享变量。这个过程包含了内存屏障的使用。
  3. 有序性 (Ordering)

    • 由于 synchronized 保证了同一时刻只有一个线程执行同步代码(“串行”执行),因此在这个线程内部,代码是顺序执行的。
    • 同时,监视器锁规则也通过内存屏障限制了一部分重排序,保证了临界区内的代码不会“逸出”到临界区之外。

代码示例:解决原子性和可见性

public class SynchronizedDemo {
    private int count = 0;

    // synchronized 保证 increment() 方法的原子性和可见性
    public synchronized void increment() {
        count++; // 原子操作,且修改对所有后续获取此锁的线程可见
    }

    public synchronized int getCount() {
        return count; // 总能读取到最新值
    }
}

synchronized 的实现原理

synchronized 通过对象监视器 (Monitor) 实现其语义。每个 Java 对象都可以作为一个监视器,线程获取监视器的所有权后才能执行 synchronized 块中的代码。

当线程进入 synchronized 块时,会执行以下操作:

  1. 检查对象的锁状态
  2. 如果对象未被锁定,线程获取锁并进入同步块
  3. 如果对象已被锁定,线程会被阻塞,直到锁被释放

当线程退出 synchronized 块时,会执行以下操作:

  1. 释放锁
  2. 将修改后的变量值刷新到主内存
  3. 唤醒其他等待该锁的线程

锁的优化

JDK 1.6之后,synchronized 引入了锁升级机制:无锁 → 偏向锁 → 轻量级锁 → 重量级锁。这种优化减少了锁操作的开销,只有在真正竞争激烈时才会升级为重量级锁。

  1. 偏向锁 (Biased Locking) :当一个线程频繁获取同一把锁时,JVM 会将锁标记为偏向该线程,避免每次获取锁都进行 CAS 操作。
  2. 轻量级锁 (Lightweight Locking) :当多个线程交替获取同一把锁时,JVM 使用轻量级锁,通过 CAS 操作尝试获取锁,避免线程阻塞。
  3. 锁粗化 (Lock Coarsening) :如果一段代码中频繁对同一个对象加锁 / 解锁,JVM 会将这些连续的锁操作合并成一次 —— 在整个代码块开始时加一次锁,结束时解锁一次。
  4. 锁消除 (Lock Elimination) :JVM 通过逃逸分析发现某个锁对象只会被一个线程访问,会自动消除该锁。

结论synchronized 是 JMM 规范的一个直接且核心的实现,它通过锁的获取和释放操作,内置了强大的内存可见性、原子性和有序性保证。

2.4 final 关键字:不可变性的安全承诺

final 关键字的行为在多线程环境下有着特殊的、由 JMM 规范保证的语义。它主要解决的是对象构造过程中的可见性和有序性问题,是一种免同步的解决方案。

它如何属于 JMM 解决方案?

JMM 为 final 域的写入和读取提供了特殊的 “初始化安全” 保证,这主要通过禁止特定重排序来实现。

  1. 禁止重排序

    • 在构造函数内部,对一个 final 域的写入(初始化),与随后将被构造对象的引用赋值给一个变量(比如 instance = new MyClass();),这两个操作不能被重排序
    • 这意味着,一旦其他线程看到了一个包含 final 域的对象引用,那么这个 final 域必然已经被构造函数正确初始化了。这完美解决了 DCL 问题中可能遇到的未初始化问题。
  2. 可见性保证

    • 一个线程构造了一个包含 final 域的对象,只要构造过程没有发生“this引用逸出”,那么当其他线程看到这个对象时,该对象的 final 字段的值一定是被构造函数写入的值,而不需要额外的同步。

代码示例:初始化安全

public class FinalFieldExample {
    final int x; // final 域,享受JMM的初始化安全保证
    int y; // 普通域,不享受
    static FinalFieldExample instance;

    public FinalFieldExample() {
        x = 42; // 1. 写入final域
        y = 50; // 2. 写入普通域
    }

    public static void writer() {
        instance = new FinalFieldExample(); // 3. 发布对象
    }

    public static void reader() {
        if (instance != null) {
            int a = instance.x; // 保证读到 42 (Thanks to JMM and final)
            int b = instance.y; // 可能读到 0 (默认值) 或 50,无法保证
        }
    }
}

注意:final 字段的“初始化安全”保证有一个重要前提:在构造过程中不能发生“this引用逸出”。即在构造函数中不得将this传递给其他线程,否则仍可能看到未初始化完成的对象。

结论final 是 JMM 提供的一种免同步的、隐式的内存可见性与有序性解决方案,但它有严格的适用范围——仅限于对象的构造阶段。它是实现安全发布不可变对象的关键。

三、实战应用:性能优化与问题排查

3.1 性能优化最佳实践

  1. 缩小同步范围:尽量使用同步块而不是同步方法,只锁必要的共享资源。
  2. volatile替代锁:如果共享变量是独立的(如状态标志位stop),且操作本身是原子的,使用volatile性能远高于synchronized
  3. 使用并发容器:优先使用ConcurrentHashMapCopyOnWriteArrayList等,它们使用了更细粒度的锁或无锁技术(如CAS)。
  4. 使用原子类:对于计数器等场景,使用AtomicIntegerLongAdder等,它们基于volatile和CAS,避免了互斥锁。

3.2 并发问题排查指南

问题类型典型现象排查工具与思路
可见性程序行为不稳定,时而正确时而错误,数据不新鲜。使用volatilesynchronized来验证猜想。使用jstack查看线程状态。
原子性计数结果总是偏小,数据不一致。检查是否存在i++等复合操作。使用原子类或同步块解决。
有序性现象极其诡异,如DCL单例拿到未初始化对象。检查是否存在指令重排序的可能。使用volatile关键字。
死锁程序卡住,无响应。使用jstackjconsole查看线程堆栈,检测锁的持有和等待关系。

推荐工具

  • jstack:打印线程堆栈信息,分析死锁。
  • jconsole / VisualVM:图形化监控线程状态、内存使用等。
  • jcstress:Java并发压力测试工具,用于测试并发代码的正确性。

总结

Java内存模型(JMM)是Java并发编程的底层核心,它通过定义主内存、工作内存的交互协议,以及提供synchronizedvolatile、Happens-Before规则等工具,来解决由于硬件优化带来的可见性原子性有序性问题。

特性volatilesynchronizedfinal (在JMM语境下)
原子性
可见性(直接保证)(通过锁规则)(仅限于构造阶段)
有序性(限制重排序)(限制重排序+串行执行)(禁止初始化重排序)
机制内存屏障锁 + 内存屏障禁止特定重排序
场景状态标志、DCL安全的复合操作、临界区安全发布、不可变对象
本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]