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

简介: 本文深入解析Java内存模型(JMM),系统阐述原子性、可见性、有序性三大核心问题,结合代码示例剖析典型并发缺陷,并详解happen-before八大规则及其在synchronized、volatile、原子类等场景中的应用,助你夯实并发编程基础。

深入拆解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);
   }
}

  1. Lock接口:与synchronized类似,提供了更灵活的锁机制。
  2. 原子类(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");
   }
}

  1. synchronized关键字:在释放锁前,会将工作内存中的变量刷新回主内存;在获取锁后,会从主内存重新读取变量。
  2. 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;
   }
}

  1. synchronized关键字:保证同一时间只有一个线程执行临界区代码,相当于让临界区代码串行执行,自然保证了有序性。
  2. 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规则,来解决并发场景下的各种问题。

目录
相关文章
|
2月前
|
存储 安全 Java
深入拆解 synchronized:从偏向锁到重量级锁的升级之旅与优化秘籍
本文深入剖析Java中synchronized的底层实现:详解偏向锁、轻量级锁到重量级锁的升级机制,结合对象头Mark Word结构、JVM锁优化(自旋、消除、粗化、逃逸分析),并附死锁排查实战,助你真正掌握并发同步原理。
168 3
|
2月前
|
SQL 运维 关系型数据库
MySQL 高可用架构终极选型指南:MGR、主从 + Keepalived、MyCat 全维度拆解与生产避坑指南
本文系统解析MySQL三大高可用架构:主从+Keepalived(成熟稳定、零侵入)、MGR(强一致、自动自愈)和MyCat(分库分表+读写分离)。涵盖核心指标(RTO/RPO/一致性/自愈能力等)、配置实例、优劣势及选型决策矩阵,助力企业精准匹配业务需求。
301 4
|
2月前
|
SQL 存储 关系型数据库
MySQL 生产级备份与恢复全攻略:全量 / 增量 / 逻辑 / 物理备份深度拆解 + 误删数据秒级恢复实战
本文系统讲解MySQL备份与恢复体系,涵盖全量/增量、逻辑/物理备份的底层原理与核心差异;详解mysqldump、mydumper、XtraBackup等工具的生产级实战;提供误删数据的多场景快速恢复方案(闪回、延迟从库、回收站);并附Java备份管理模块完整实现。
455 2
|
XML Java 数据库连接
IDEA添加Mapper.xml文件模板
IDEA添加Mapper.xml文件模板
IDEA添加Mapper.xml文件模板
|
5小时前
|
存储 SQL 安全
【Java并发编程】JMM Java内存模型:原子性、可见性、有序性、happens-before原则(附《思维导图》+《面试高频考点清单》)
Java内存模型(JMM)是Java并发编程的基石,抽象定义主内存与线程工作内存的交互规则,系统解决可见性、原子性、有序性三大核心问题,并通过happens-before、volatile、synchronized等机制保障多线程安全与跨平台一致性。
|
6月前
|
存储 缓存 监控
从GC日志小白到分析大神:GCEasy实战全攻略
GCEasy是Java GC日志分析利器,支持多种垃圾收集器,通过可视化报表与智能诊断,帮助开发者快速定位内存泄漏、GC频繁等问题。本文结合实战案例,详解其原理、使用方法及性能优化策略,提升系统稳定性与并发能力。
990 1
|
2月前
|
编译器 C语言 C++
平凡与标准布局——C++内存模型的隐秘角落
在C++的类型系统中,有一组看似不起眼却至关重要的概念:平凡类型、标准布局类型、平凡可复制类型、以及POD类型。
700 2
|
6月前
|
数据采集 存储 安全
DAMA数据管理导论-数据管理的本质及价值
数据管理是将数据转化为战略资产的系统方法,强调主动治理而非被动存储。通过提升数据质量、强化元数据管理、推动跨部门协作,企业可实现从直觉决策到数据驱动的跃迁,释放数据在营销、产品、人力等场景的深层价值。
|
监控 网络协议 JavaScript
【公告】淘宝 npm 域名即将切换 && npmmirror 重构升级
淘宝NPM 镜像站喊你切换新域名啦。新的Web 站点:https://npmmirror.com,Registry Endpoint:https://registry.npmmirror.com。 http://npm.taobao.org 和 http://registry.npm.taobao.org 将在 2022.06.30 号正式下线和停止 DNS 解析。
4038 0
|
XML Java Maven
Maven 构建配置文件
Maven构建配置文件用于定制不同环境的构建,如生产与开发。配置在`pom.xml`的`profiles`中,可通过命令行、设置文件、环境变量等方式激活。配置文件分项目级、用户级和全局级。例如,`env.properties`为默认,`env.test.properties`和`env.prod.properties`代表测试和生产环境。激活配置文件可影响如数据库URL等参数。示例中用AntRun插件展示配置应用,但实际配置文件功能不限于此。