授课语音

Java 中的 JMM(Java Memory Model)

Java 内存模型(JMM,Java Memory Model)是 Java 程序中的一种重要机制,旨在定义 Java 程序中的线程如何访问和共享内存。它解决了多线程并发执行时,如何保证不同线程之间的数据一致性和正确性的问题。JMM 为 Java 程序员提供了一个一致的视图,避免了硬件和操作系统差异带来的影响。


1. JMM 概述

1.1 什么是 JMM

Java 内存模型(JMM)定义了 Java 程序中各种变量的访问规则,特别是多线程情况下,如何确保不同线程对共享变量的正确访问。JMM 为程序员提供了一组规则来规范线程之间的交互,以便可以确保程序的正确性和一致性。

JMM 主要目标:

  1. 保证线程之间的可见性:即一个线程对共享变量的修改,能及时被其他线程看到。
  2. 保证线程之间的顺序性:即程序执行的顺序和线程执行的顺序能保持一致。
  3. 避免乱序执行:通过适当的同步手段,保证操作的顺序性,避免硬件层面的乱序执行。

1.2 JMM 中的关键概念

  • 主内存(Main Memory):主内存是所有线程共享的内存区域。它存放了程序中的变量(实例字段、静态字段、线程共享的变量等)。
  • 工作内存(Working Memory):每个线程有自己的工作内存,工作内存保存着该线程的局部变量以及从主内存中读取到的共享变量的副本。线程对变量的操作,实际上是对工作内存中的副本进行操作。
  • 共享变量:多个线程可以访问和修改的变量。

2. JMM 中的同步机制

为了保证多线程访问共享变量时的可见性和有序性,Java 提供了多种同步机制。主要的同步机制有:

2.1 volatile 关键字

volatile 关键字用于修饰变量,表示该变量的值在多线程之间是共享的。当一个线程修改 volatile 变量的值时,其他线程能够立刻看到最新的值。

特点:

  • volatile 保证变量的可见性。
  • 不能保证原子性(例如,对 volatile 变量的写操作不是原子性的)。

示例:

public class VolatileExample {
    private static volatile boolean flag = false;

    public static void main(String[] args) {
        // 创建一个线程,改变 flag 的值
        new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            flag = true;  // 修改 flag 为 true
            System.out.println("flag has been set to true");
        }).start();

        // 主线程检查 flag 的值
        while (!flag) {
            // 主线程等待 flag 为 true
        }

        System.out.println("Main thread detected flag is true");
    }
}

代码解析:

  • 在上面的代码中,volatile 关键字保证了 flag 的修改对所有线程立即可见。
  • 主线程一直在等待 flagtrue,一旦子线程修改 flag 的值,主线程可以立即看到变化,并退出循环。

输出示例:

flag has been set to true
Main thread detected flag is true

2.2 synchronized 关键字

synchronized 关键字用于修饰方法或代码块,用来确保在同一时刻,只有一个线程能够执行被 synchronized 修饰的代码区域。

特点:

  • 保证代码块的原子性。
  • 只要一个线程进入 synchronized 方法或代码块,其他线程将无法访问该方法或代码块。
  • 可以用来控制对共享资源的访问,保证线程安全。

示例:

public class SynchronizedExample {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public static void main(String[] args) {
        SynchronizedExample example = new SynchronizedExample();

        // 创建多个线程同时访问 increment 方法
        for (int i = 0; i < 1000; i++) {
            new Thread(example::increment).start();
        }

        // 暂停一段时间,确保所有线程执行完毕
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("count: " + example.count);  // 输出结果应为 1000
    }
}

代码解析:

  • synchronized 保证了 increment() 方法在同一时刻只会被一个线程执行,其他线程会等待该线程执行完毕,保证了 count 变量的线程安全。

输出示例:

count: 1000

2.3 final 关键字与 JMM

在 JMM 中,final 关键字也有特殊的含义,特别是在多线程环境下。对于一个被声明为 final 的变量,它的值一旦初始化后,将不能再被修改。

特点:

  • final 变量的初始化发生在构造函数或类初始化时,JMM 保证在构造函数执行完毕之后,所有线程可以看到该 final 变量的正确值。

示例:

public class FinalExample {
    private final int value;

    public FinalExample(int value) {
        this.value = value;  // 在构造器中初始化
    }

    public static void main(String[] args) {
        FinalExample example = new FinalExample(10);
        System.out.println("Final value: " + example.value);
    }
}

代码解析:

  • final 关键字保证了 value 在对象构造完成后不会再被修改,同时 JMM 保证所有线程看到的 value 值一致。

输出示例:

Final value: 10

3. JMM 的一些应用场景

3.1 双重检查锁(Double-Checked Locking)

双重检查锁定是一种在多线程环境下提高性能的技术。通过在获取锁之前进行检查,减少不必要的锁操作。

示例:

public class Singleton {
    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;
    }
}

代码解析:

  • 使用 volatile 关键字,确保线程在检查 instance 时看到的是最新的实例,避免在多线程环境中创建多个对象。

4. 总结

  • JMM 定义了多线程中共享变量的访问规则,确保线程之间的可见性和有序性。
  • 通过 volatile 关键字保证变量的可见性,避免线程缓存的副本过时。
  • synchronized 保证了线程同步,防止并发修改带来的数据不一致。
  • final 确保变量在多线程环境中初始化后的值不会改变,提升程序的安全性和稳定性。

理解 JMM 是开发高效、稳定和线程安全 Java 应用的基础。

去1:1私密咨询

系列课程: