Java JMM案例

wen java案例 3

本文目录导读:

Java JMM案例

  1. 目录导读
  2. JMM是什么?——为什么并发编程必须依赖它
  3. 三大核心特性:原子性、可见性、有序性(附案例)
  4. 经典故障复盘:i++ 为什么不是线程安全的?
  5. volatile与synchronized的底层内存语义对比
  6. Happens-Before规则:判定数据竞争的唯一法律
  7. 实战演练:用JMM设计一个无锁计数器
  8. 常见问答(FAQ)
  9. 结语:JMM是并发编程的“交通规则”

Java JMM实战指南:从可见性陷阱到内存屏障,彻底搞懂并发编程的“幕后规则”


目录导读

  1. JMM是什么?——为什么并发编程必须依赖它
  2. 三大核心特性:原子性、可见性、有序性(附案例)
  3. 经典故障复盘:i++ 为什么不是线程安全的?
  4. volatile与synchronized的底层内存语义对比
  5. Happens-Before规则:判定数据竞争的唯一法律
  6. 实战演练:用JMM设计一个无锁计数器
  7. 常见问答(FAQ)

JMM是什么?——为什么并发编程必须依赖它

Java内存模型(Java Memory Model, JMM)是Java虚拟机规范中定义的一组抽象规则,用于屏蔽不同硬件和操作系统之间的内存访问差异,它规定了共享变量(堆内存中的实例字段、静态字段、数组元素)在多线程环境下的读写行为。

关键概念:

  • 主内存:所有线程共享的内存区域,存储变量原始值。
  • 工作内存:每个线程私有的高速缓存(CPU缓存/寄存器),存储变量的副本。

核心矛盾:线程对变量的操作(读取、赋值)必须先拷贝到工作内存,再同步回主内存,若多个线程同时操作同一变量,必然导致数据不一致,JMM通过内存屏障同步规则来管理这一过程。


三大核心特性:原子性、可见性、有序性(附案例)

原子性(Atomicity)

  • 定义:一个操作或多个操作要么全部执行且不被中断,要么都不执行。
  • JMM保障synchronized块内代码、LockAtomicInteger等。
  • 反例i++包含“读-改-写”三步,非原子操作。

可见性(Visibility)

  • 定义:一个线程修改共享变量后,其他线程能立刻看到。
  • 案例
    public class VisibilityDemo {  
      private static boolean flag = false;  
      public static void main(String[] args) throws InterruptedException {  
          new Thread(() -> {  
              while (!flag) {  
                  // 空循环  
              }  
              System.out.println("线程结束!");  
          }).start();  
          Thread.sleep(100);  
          flag = true;  // 主线程修改flag  
      }  
    }  

    现象:子线程可能永远不退出,因为主线程修改的flag只是写入了主内存,但子线程的工作内存中flag副本仍为false解决方案:用volatile关键字修饰flag,强制每次读取都从主内存获取。

有序性(Ordering)

  • 定义:程序执行顺序按照代码先后顺序。
  • 隐患:指令重排序(JIT编译器、CPU乱序执行)可能导致程序行为变化。
  • 案例
    int a = 0;  
    boolean init = false;  
    // 线程A  
    a = 42;        // 1  
    init = true;   // 2  
    // 线程B  
    while (!init);  
    System.out.println(a);  

    若2在1之前重排序,线程B可能读到a=0


经典故障复盘:i++ 为什么不是线程安全的?

代码

public class Counter {  
    private int count = 0;  
    public void increment() { count++; }  
}  

底层操作分解

  1. 从主内存读取count到工作内存。
  2. 在工作内存中计算count+1
  3. 将结果写回主内存。

并发场景:线程A和线程B同时执行上述步骤,可能都读到了count=0,各自计算后写回count=1,实际期望是2。根因:非原子操作+可见性缺失。

修复方案:使用synchronizedAtomicIntegervolatile(仅适用于单写场景)。


volatile与synchronized的底层内存语义对比

特性 volatile synchronized
原子性 不保证 保证(锁内代码)
可见性 强制每次读主内存 解锁时刷新工作内存到主内存
有序性 禁止指令重排序(加了内存屏障) 锁的acquire/release保障
适用场景 状态标志位、单写多读 多线程复合操作(如i++)

内存屏障示例volatile写操作后插入StoreStore屏障,读操作前插入LoadLoad屏障,防止重排序。


Happens-Before规则:判定数据竞争的唯一法律

JMM规定八条规则,若两个操作满足Happens-Before关系,则前一个操作的结果对后一个操作可见。重点规则

  1. 程序次序规则:同一线程内,代码顺序即执行顺序。
  2. 锁规则:解锁操作Happens-Before于后续加锁。
  3. volatile规则:volatile变量的写Happens-Before于后续读。
  4. 传递性:若A→B,B→C,则A→C。

实践意义:通过以上规则组合,开发者无需在每次同步操作后手动刷新内存,只需保证代码满足规则即可。


实战演练:用JMM设计一个无锁计数器

import java.util.concurrent.atomic.AtomicInteger;  
public class LockFreeCounter {  
    private final AtomicInteger count = new AtomicInteger(0);  
    public void increment() {  
        count.incrementAndGet();  // CAS操作,原子性+可见性  
    }  
    public int getCount() {  
        return count.get();  // volatile读  
    }  
}  
  • 原理:CAS(Compare-And-Swap)利用底层原子指令(如x86的LOCK CMPXCHG)保证原子性,同时AtomicInteger内部使用volatile修饰value,确保可见性。

常见问答(FAQ)

Q1:为什么longdouble类型在32位JVM上可能不是原子的?
A:JMM规定,非volatile的64位写操作在32位平台可能拆成两次32位写,导致读到中间值,但商用JVM通常已实现原子化(如HotSpot)。

Q2:重排序会出现在单线程中吗?为什么?
A:会,只要最终执行结果与顺序执行一致(as-if-serial语义),编译器可自由优化。

int a = 1;  
int b = 2;  
int c = a + b;  // 重排序后a、b的赋值顺序可交换  

Q3:synchronizedReentrantLock的JMM实现有何不同?
A:二者在内存语义上等价——都保证锁内变量刷新到主内存,但ReentrantLock提供非阻塞尝试tryLock(),以及公平锁选项。

Q4:如何用JMM解释“死锁”不属于内存问题?
A:死锁是线程调度问题(互相持有锁等待),与可见性/原子性无关,但JMM的锁规则要求解锁可见于后续加锁,这间接促使线程安全设计。

Q5:final字段是否有JMM保障?
A:有,JMM规定:构造函数中赋值的final字段,在对象发布后对其他线程可见(无需同步),且禁止将final字段的初始值重排序到构造函数外。


JMM是并发编程的“交通规则”

理解JMM不是去死记概念,而是通过真实案例建立直觉:何时需要同步、如何选择工具、如何避免数据竞争,建议读者用一个多线程计数器demo,逐步尝试volatilesynchronizedAtomicInteger,观察性能差异与正确性变化——实践是掌握并发的最快路径。

抱歉,评论功能暂时关闭!