深入拆解 Java 内存模型:从原子性、可见性到有序性,彻底搞懂 happen-before 规则

0 阅读10分钟

深入拆解Java内存模型:从原子性、可见性到有序性,彻底搞懂happen-before规则

在Java并发编程中,Java内存模型(JMM)是最核心的概念之一。它不仅定义了线程与主内存之间的抽象关系,还为解决并发场景下的原子性、可见性、有序性问题提供了规范保障。理解JMM,是写出正确、高效并发代码的基础。

JMM将内存分为主内存(Main Memory)和工作内存(Working Memory)。主内存是所有线程共享的,存储了实例对象、静态变量等数据;而每个线程都有自己私有的工作内存,存储了该线程使用的变量的主内存副本。线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,不能直接读写主内存。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递需要通过主内存来完成。

原子性:不可分割的操作

原子性是指一个操作是不可分割的,要么全部执行成功,要么全部不执行,执行过程中不会被其他线程打断。

在Java中,对基本数据类型的读取和赋值操作通常是原子性的,但像count++这样的复合操作(读取-修改-写入)就不具备原子性。

示例:原子性问题

package com.jam.demo;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.CountDownLatch;

/**
 * 原子性示例
 *
 * @author ken
 */
@Slf4j
public class AtomicityDemo {
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        int threadCount = 1000;
        CountDownLatch countDownLatch = new CountDownLatch(threadCount);

        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                try {
                    for (int j = 0; j < 1000; j++) {
                        count++;
                    }
                } finally {
                    countDownLatch.countDown();
                }
            }).start();
        }

        countDownLatch.await();
        log.info("count: {}", count);
    }
}

这段代码启动1000个线程,每个线程对count进行1000次自增操作,预期结果是1000000,但实际运行结果往往小于这个值,因为count++是复合操作,不具备原子性。

保证原子性的方式

  1. synchronized关键字:通过管程(Monitor)机制保证同一时间只有一个线程能执行临界区代码。
package com.jam.demo;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.CountDownLatch;

/**
 * synchronized保证原子性示例
 *
 * @author ken
 */
@Slf4j
public class SynchronizedAtomicityDemo {
    private static int count = 0;

    private static synchronized void increment() {
        count++;
    }

    public static void main(String[] args) throws InterruptedException {
        int threadCount = 1000;
        CountDownLatch countDownLatch = new CountDownLatch(threadCount);

        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                try {
                    for (int j = 0; j < 1000; j++) {
                        increment();
                    }
                } finally {
                    countDownLatch.countDown();
                }
            }).start();
        }

        countDownLatch.await();
        log.info("count: {}", count);
    }
}

2. Lock接口:与synchronized类似,提供了更灵活的锁机制。 3. 原子类(java.util.concurrent.atomic) :基于CAS(Compare-And-Swap)操作实现无锁原子性。

package com.jam.demo;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * AtomicInteger保证原子性示例
 *
 * @author ken
 */
@Slf4j
public class AtomicIntegerDemo {
    private static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        int threadCount = 1000;
        CountDownLatch countDownLatch = new CountDownLatch(threadCount);

        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                try {
                    for (int j = 0; j < 1000; j++) {
                        count.incrementAndGet();
                    }
                } finally {
                    countDownLatch.countDown();
                }
            }).start();
        }

        countDownLatch.await();
        log.info("count: {}", count.get());
    }
}

可见性:线程间的变量同步

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

CPU缓存模型与可见性问题

现代CPU通常有多级缓存(L1、L2、L3),每个核心有自己的L1、L2缓存,多个核心共享L3缓存。当线程修改变量时,会先将变量从主内存加载到工作内存(对应CPU缓存),修改后写回主内存,但写回时机不确定,导致其他线程可能看不到最新值。

示例:可见性问题

package com.jam.demo;

import lombok.extern.slf4j.Slf4j;

/**
 * 可见性问题示例
 *
 * @author ken
 */
@Slf4j
public class VisibilityDemo {
    private static boolean flag = false;

    public static void main(String[] args) {
        new Thread(() -> {
            while (!flag) {
                // 空循环
            }
            log.info("线程A检测到flag变为true,结束循环");
        }, "线程A").start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            log.error("线程中断", e);
        }

        flag = true;
        log.info("主线程将flag设置为true");
    }
}

这段代码中,主线程将flag设置为true后,线程A可能永远不会结束循环,因为线程A的工作内存中flag还是旧值。

保证可见性的方式

  1. volatile关键字:通过内存屏障(Memory Barrier)保证变量的可见性。当写一个volatile变量时,JMM会把该线程工作内存中的变量值立即刷新回主内存;当读一个volatile变量时,JMM会把该线程工作内存中的变量置为无效,重新从主内存读取。
package com.jam.demo;

import lombok.extern.slf4j.Slf4j;

/**
 * volatile保证可见性示例
 *
 * @author ken
 */
@Slf4j
public class VolatileVisibilityDemo {
    private static volatile boolean flag = false;

    public static void main(String[] args) {
        new Thread(() -> {
            while (!flag) {
                // 空循环
            }
            log.info("线程A检测到flag变为true,结束循环");
        }, "线程A").start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            log.error("线程中断", e);
        }

        flag = true;
        log.info("主线程将flag设置为true");
    }
}

2. synchronized关键字:在释放锁前,会将工作内存中的变量刷新回主内存;在获取锁后,会从主内存重新读取变量。 3. final关键字:final修饰的字段在初始化完成后,其他线程能看到其正确值(前提是对象没有逸出)。

有序性:禁止指令重排序

有序性是指程序执行的顺序按照代码的先后顺序执行。但在并发场景下,编译器和CPU可能会对指令进行重排序,以提高性能,这可能导致程序执行结果与预期不符。

指令重排序

  • 编译器重排序:编译器在不改变单线程程序语义的前提下,调整指令的执行顺序。
  • CPU重排序:CPU在执行指令时,可能会调整指令的执行顺序,以充分利用CPU流水线。

as-if-serial语义

as-if-serial语义保证:不管怎么重排序,单线程程序的执行结果不能被改变。编译器、runtime和CPU都必须遵守as-if-serial语义。

示例:有序性问题(双重检查锁定)

package com.jam.demo;

/**
 * 双重检查锁定示例(有序性问题)
 *
 * @author ken
 */
public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

    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指向分配的内存地址

重排序后可能变成:

  1. 分配内存空间
  2. 将instance指向分配的内存地址
  3. 初始化对象

如果线程A执行到步骤2,此时instance不为null,但还没初始化对象,线程B在第一次检查时发现instance不为null,直接返回,就会拿到一个未初始化的对象。

保证有序性的方式

  1. volatile关键字:通过内存屏障禁止指令重排序。对于volatile变量的写操作,会在写操作前插入StoreStore屏障,写操作后插入StoreLoad屏障;对于volatile变量的读操作,会在读操作前插入LoadLoad屏障,读操作后插入LoadStore屏障。 修改后的双重检查锁定:
package com.jam.demo;

/**
 * 双重检查锁定示例(volatile保证有序性)
 *
 * @author ken
 */
public class VolatileSingleton {
    private static volatile VolatileSingleton instance;

    private VolatileSingleton() {
    }

    public static VolatileSingleton getInstance() {
        if (instance == null) {
            synchronized (VolatileSingleton.class) {
                if (instance == null) {
                    instance = new VolatileSingleton();
                }
            }
        }
        return instance;
    }
}

2. synchronized关键字:保证同一时间只有一个线程执行临界区代码,相当于让临界区代码串行执行,自然保证了有序性。 3. happen-before规则:JMM定义的一套偏序关系,通过这些规则可以判断两个操作是否有序。

happen-before规则:JMM的核心偏序关系

happen-before规则是JMM定义的一套偏序关系,用于判断两个操作之间是否存在可见性保证。如果操作A happen-before 操作B,那么A的执行结果对B可见,且A的执行顺序排在B之前。

1. 程序次序规则

在一个线程内,按照代码顺序,前面的操作happen-before于后面的操作。

package com.jam.demo;

import lombok.extern.slf4j.Slf4j;

/**
 * 程序次序规则示例
 *
 * @author ken
 */
@Slf4j
public class ProgramOrderRuleDemo {
    public static void main(String[] args) {
        int a = 1;
        int b = 2;
        int c = a + b;
        log.info("c: {}", c);
    }
}

在主线程中,int a = 1 happen-before int b = 2int b = 2 happen-before int c = a + b,所以a和b的赋值对c的计算可见。

2. 管程锁定规则

一个unlock操作happen-before于后面对同一个锁的lock操作。

package com.jam.demo;

import lombok.extern.slf4j.Slf4j;

/**
 * 管程锁定规则示例
 *
 * @author ken
 */
@Slf4j
public class MonitorLockRuleDemo {
    private static int count = 0;

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (MonitorLockRuleDemo.class) {
                count = 10;
            }
        }, "线程A").start();

        new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                log.error("线程中断", e);
            }
            synchronized (MonitorLockRuleDemo.class) {
                log.info("count: {}", count);
            }
        }, "线程B").start();
    }
}

线程A先释放锁,线程B后获取锁,所以线程A对count的修改对线程B可见。

3. volatile变量规则

对一个volatile变量的写操作happen-before于后面对这个变量的读操作。

package com.jam.demo;

import lombok.extern.slf4j.Slf4j;

/**
 * volatile变量规则示例
 *
 * @author ken
 */
@Slf4j
public class VolatileVariableRuleDemo {
    private static volatile int value = 0;
    private static boolean flag = false;

    public static void main(String[] args) {
        new Thread(() -> {
            value = 10;
            flag = true;
        }, "线程A").start();

        new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                log.error("线程中断", e);
            }
            if (flag) {
                log.info("value: {}", value);
            }
        }, "线程B").start();
    }
}

这里flag是volatile变量,线程A对flag的写操作happen-before线程B对flag的读操作,根据传递性,线程A对value的修改对线程B可见。

4. 线程启动规则

Thread对象的start()方法happen-before于此线程的每一个动作。

package com.jam.demo;

import lombok.extern.slf4j.Slf4j;

/**
 * 线程启动规则示例
 *
 * @author ken
 */
@Slf4j
public class ThreadStartRuleDemo {
    private static int value = 0;

    public static void main(String[] args) {
        value = 10;
        Thread thread = new Thread(() -> {
            log.info("value: {}", value);
        }, "线程A");
        thread.start();
    }
}

主线程对value的修改happen-before线程A的start()方法,线程A的start()方法happen-before线程A的所有动作,所以主线程对value的修改对线程A可见。

5. 线程终止规则

线程中的所有操作都happen-before于对此线程的终止检测。

package com.jam.demo;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.CountDownLatch;

/**
 * 线程终止规则示例
 *
 * @author ken
 */
@Slf4j
public class ThreadTerminationRuleDemo {
    private static int value = 0;

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(1);
        Thread thread = new Thread(() -> {
            try {
                value = 10;
            } finally {
                countDownLatch.countDown();
            }
        }, "线程A");
        thread.start();
        countDownLatch.await();
        log.info("value: {}", value);
    }
}

线程A的所有操作happen-before主线程对线程A的终止检测(通过CountDownLatch.await()),所以线程A对value的修改对主线程可见。

6. 线程中断规则

对线程interrupt()方法的调用happen-before于被中断线程的代码检测到中断事件的发生。

package com.jam.demo;

import lombok.extern.slf4j.Slf4j;

/**
 * 线程中断规则示例
 *
 * @author ken
 */
@Slf4j
public class ThreadInterruptRuleDemo {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                // 空循环
            }
            log.info("线程A检测到中断,结束循环");
        }, "线程A");
        thread.start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            log.error("线程中断", e);
        }

        thread.interrupt();
    }
}

主线程对thread的interrupt()调用happen-before线程A检测到中断事件,所以线程A能正确响应中断。

7. 对象终结规则

一个对象的初始化完成happen-before于它的finalize()方法的开始。

package com.jam.demo;

import lombok.extern.slf4j.Slf4j;

/**
 * 对象终结规则示例
 *
 * @author ken
 */
@Slf4j
public class ObjectFinalizeRuleDemo {
    private int value;

    public ObjectFinalizeRuleDemo() {
        this.value = 10;
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        log.info("value: {}", value);
    }

    public static void main(String[] args) {
        new ObjectFinalizeRuleDemo();
        System.gc();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            log.error("线程中断", e);
        }
    }
}

对象的初始化完成happen-beforefinalize()方法的开始,所以finalize()方法中能看到value的正确值。

8. 传递性

如果A happen-before B,B happen-before C,那么A happen-before C。 在volatile变量规则的示例中,线程A对value的修改(A)happen-before线程A对flag的写(B),线程A对flag的写(B)happen-before线程B对flag的读(C),所以线程A对value的修改(A)happen-before线程B对value的读(C)。

总结

JMM通过主内存与工作内存的抽象结构,定义了原子性、可见性、有序性的规范,并通过happen-before规则为并发编程提供了可见性保证。理解JMM的核心概念和规则,是写出正确、高效并发代码的关键。在实际开发中,我们可以通过synchronized、volatile、Lock、原子类等工具,结合happen-before规则,来解决并发场景下的各种问题。