JMM内存模型介绍

Java 内存模型(Java Memory Model,JMM)是 Java 规范的一部分,它定义了 Java 程序在多线程环境下的行为,特别是关于线程之间共享变量的访问和修改。JMM 的目的是确保 Java 程序在多线程环境中具有一致性、可见性和有序性。

1. JMM 的核心概念

JMM 定义了如何处理线程间共享变量的访问,包括以下几个重要概念:

  • 主内存:共享变量存储的位置,所有线程都能访问。
  • 工作内存:每个线程的私有内存,存储该线程的局部变量、寄存器等数据。线程操作共享变量时,会先从主内存加载数据到工作内存中进行操作。
  • 同步操作:通过 synchronizedvolatilefinal 等关键词,保证线程间的操作顺序和可见性。
  • 共享变量:在多个线程间共享的数据。

2. JMM 的基本原则

  1. 原子性:保证基本操作(如赋值、加法、减法等)在多线程环境下是不可分割的,不会被线程调度中断。
  2. 可见性:一个线程对共享变量的修改,能够及时反映到其他线程中。
  3. 有序性:程序中的操作应该按照代码顺序执行,但由于编译优化和CPU优化,实际执行顺序可能会有所调整。JMM 保证通过适当的同步机制,线程之间的操作顺序是可控的。

3. JMM 的实现机制

JMM 通过一些底层机制来实现线程之间的同步、共享和顺序:

  • 缓存一致性协议:如 MESI(Modified, Exclusive, Shared, Invalid)协议,确保每个线程的工作内存与主内存保持一致。
  • 内存屏障(Memory Barrier):JMM 会插入一些内存屏障,确保指令执行的顺序符合预期。
  • happens-before 关系:JMM 定义了哪些操作必须先发生,哪些操作可以后发生,以保证程序在多线程环境中的正确性。

4. JMM 中的关键字和机制

4.1 volatile 关键字

volatile 关键字用于保证变量的 可见性禁止指令重排,即确保线程对变量的修改可以被其他线程立即看到。对 volatile 变量的读写操作是 原子 的,但不保证复合操作的原子性(例如 ++ 操作不是原子的)。

  • 可见性:当一个线程修改了 volatile 变量的值,其他线程能立即看到这个变化。
  • 禁止指令重排:禁止 volatile 变量前后的代码进行重排,保证指令的执行顺序。

示例代码:

class VolatileExample {
    private volatile boolean flag = false;

    public void setFlag() {
        flag = true;  // 修改为 volatile
    }

    public void checkFlag() {
        while (!flag) {
            // 线程 1 会不停等待 flag 被更新
        }
        System.out.println("Flag has been updated to true");
    }

    public static void main(String[] args) throws InterruptedException {
        VolatileExample example = new VolatileExample();
        
        Thread t1 = new Thread(example::checkFlag);
        Thread t2 = new Thread(example::setFlag);
        
        t1.start();
        Thread.sleep(1000);  // 确保 t1 线程先执行
        t2.start();
    }
}

解释:

  • 线程 t1 会持续检查 flag 是否为 true,而线程 t2 会将 flag 设置为 true
  • 由于 flagvolatile 变量,线程 t1 会看到线程 t2 修改的 flag 值,避免了不可见性问题。

4.2 synchronized 关键字

synchronized 关键字用于保证 原子性可见性有序性,常用来实现临界区保护,确保多个线程在访问共享资源时不发生冲突。

  • 原子性:保证在同一时刻只有一个线程能够进入 synchronized 代码块。
  • 可见性:进入 synchronized 代码块前,线程会将工作内存中的修改刷新到主内存;退出时,会将主内存中的数据更新到工作内存。
  • 有序性:保证线程的执行顺序,避免指令重排。

示例代码:

class SynchronizedExample {
    private int counter = 0;

    public synchronized void increment() {
        counter++;  // 线程安全的加法操作
    }

    public synchronized int getCounter() {
        return counter;
    }

    public static void main(String[] args) throws InterruptedException {
        SynchronizedExample example = new SynchronizedExample();
        
        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                example.increment();
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("Counter: " + example.getCounter());
    }
}
  • 使用 synchronized 关键字保证 counter 变量的修改是线程安全的,避免了并发问题。
  • 通过对 increment() 方法加锁,确保同一时刻只有一个线程能够进入该方法,保证了原子性。

4.3 final 关键字

final 关键字在 JMM 中有特别的作用,主要用于 保证可见性禁止指令重排,特别是对于对象的引用和基本数据类型的初始化。

  • 对于 基本数据类型final 保证了变量的值在构造完成后不被改变,并且会立即对其他线程可见。
  • 对于 引用类型final 保证了引用指向的对象不变,但对象内部的内容可以变化。

5. JMM 的 happens-before 关系

JMM 中的 happens-before 关系规定了操作之间的先后顺序,确保线程间的同步行为。主要有以下几种:

  1. 程序顺序规则:同一线程中的每个操作都 happens-before 后续的操作。
  2. 监视器锁规则:一个线程解锁一个对象的锁,另一个线程在获取该锁之前,必须看到该线程对共享变量的修改。
  3. volatile 变量规则:一个线程写入 volatile 变量,另一个线程读取该 volatile 变量时,必须看到写入线程的最新值。
  4. 线程启动规则:调用 Thread.start() 之后,线程的 run() 方法中的所有操作都会 happens-before 线程结束之前的操作。
  5. 线程终止规则:一个线程的 Thread.join() 结束,必须在该线程执行完毕后执行。

6. JMM 示例:线程安全的计数器

假设我们有一个多线程环境中的计数器,我们希望确保对该计数器的修改是线程安全的,可以使用 volatilesynchronized 来解决。

class ThreadSafeCounter {
    private volatile int counter = 0;

    public synchronized void increment() {
        counter++;  // 原子性
    }

    public int getCounter() {
        return counter;
    }

    public static void main(String[] args) throws InterruptedException {
        ThreadSafeCounter counter = new ThreadSafeCounter();
        
        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("Final Counter: " + counter.getCounter());  // 2000
    }
}
#牛客创作赏金赛#
Java碎碎念 文章被收录于专栏

来一杯咖啡,聊聊Java的碎碎念呀

全部评论

相关推荐

评论
1
4
分享

创作者周榜

更多
牛客网
牛客企业服务