13 Java内存模型:volatile与并发安全

图片[1]-13 Java内存模型:volatile与并发安全-速优课

Java 内存模型(JMM)定义了 Java 程序中变量的访问规则,特别是在多线程环境下的可见性和有序性。理解 JMM 是编写线程安全代码的基础。本文将深入探讨 JMM 的核心概念、volatile 关键字的作用以及并发安全的保障机制。

一、Java 内存模型概述

JMM 的核心目标

JMM 的主要目标是定义程序中变量的访问规则,确保在多线程环境下的:

  1. 原子性:操作不可分割
  2. 可见性:一个线程的修改对其他线程可见
  3. 有序性:操作按预期顺序执行

JMM 的抽象结构

JMM 将内存分为主内存工作内存

┌─────────────────────────────────────────────────────────────┐
│                    主内存(Main Memory)                     │
│  ┌─────────────────────────────────────────────────────┐   │
│  │     共享变量:instanceCount、sharedData、...          │   │
│  └─────────────────────────────────────────────────────┘   │
├─────────────────────────────────────────────────────────────┤
│  ┌─────────────┐      ┌─────────────┐                      │
│  │   线程1      │      │   线程2     │                      │
│  │  工作内存     │      │  工作内存   │                      │
│  │  (寄存器、    │      │  (寄存器、  │                      │
│  │   缓存)      │      │   缓存)    │                       │
│  │              │      │             │                     │
│  │ copy of      │      │ copy of     │                     │
│  │ instanceCount│      │ instanceCount│                    │
│  └─────────────┘      └─────────────┘                      │
└─────────────────────────────────────────────────────────────┘

JMM 的操作规则

操作说明
lock将主内存中的变量锁定
unlock解锁主内存中的变量
read从主内存读取变量到工作内存
load将 read 的值放入工作内存变量副本
use使用工作内存中的变量值
assign将新值赋值给工作内存中的变量
store将工作内存中的变量值写入主内存
write将 store 的值写入主内存变量

二、volatile 关键字

volatile 的作用

volatile 关键字用于保证变量的可见性有序性

  1. 可见性:当一个线程修改了 volatile 变量的值,新值对其他线程立即可见
  2. 有序性:禁止指令重排序,保证操作的顺序执行

volatile 的内存语义

写操作语义

  • 将工作内存中的变量值刷新到主内存
  • 使其他线程的工作内存中的变量副本失效

读操作语义

  • 从主内存读取变量值到工作内存
  • 不使用工作内存中的缓存值

volatile vs synchronized

特性volatilesynchronized
原子性不保证保证
可见性保证保证
有序性保证保证
锁机制
性能开销

volatile 的使用场景

// 场景1:状态标志
public class VolatileFlag {
    private volatile boolean running = true;
    
    public void stop() {
        running = false;
    }
    
    public void run() {
        while (running) {
            // 业务逻辑
        }
    }
}
​
// 场景2:双重检查锁定(DCL)
public class Singleton {
    private volatile static Singleton instance;
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        if (instance == null) {           // 第一次检查
            synchronized (Singleton.class) {
                if (instance == null) {   // 第二次检查
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

volatile 为什么需要在 DCL 中使用

instance = new Singleton() 不是原子操作,分为三步:

  1. 分配内存空间
  2. 初始化对象
  3. 将引用指向内存空间

如果没有 volatile,可能发生指令重排序:步骤 2 和步骤 3 交换,导致其他线程获取到未初始化的对象。

三、原子性、可见性与有序性

原子性

定义:操作不可分割,要么全部执行,要么全部不执行

Java 中的原子操作

  • 基本类型的读取和赋值(除 long 和 double)
  • volatile 变量的读写
  • 使用原子类(AtomicInteger、AtomicLong 等)

非原子操作示例

int count = 0;
count++;  // 非原子操作:读取、加1、写入

可见性

定义:一个线程的修改对其他线程可见

保证可见性的方式

  • 使用 volatile 关键字
  • 使用 synchronized 关键字
  • 使用 Lock 接口

可见性问题示例

public class VisibilityProblem {
    private boolean flag = false;  // 没有 volatile
    
    public void writer() {
        flag = true;  // 修改后可能没有刷新到主内存
    }
    
    public void reader() {
        while (!flag) {  // 可能读取到缓存中的旧值
            // 死循环
        }
    }
}

有序性

定义:操作按预期顺序执行

指令重排序

  • JVM 和 CPU 为了优化性能,可能对指令进行重排序
  • 重排序不影响单线程执行结果,但可能影响多线程

保证有序性的方式

  • 使用 volatile 关键字(禁止重排序)
  • 使用 synchronized 关键字(建立 happens-before 关系)
  • 使用 final 关键字(保证构造器内的初始化完成)

四、happens-before 原则

happens-before 的定义

如果操作 A happens-before 操作 B,那么操作 A 的结果对操作 B 可见,且操作 A 的顺序在操作 B 之前。

常见的 happens-before 关系

规则说明
程序顺序规则同一个线程中,前面的操作 happens-before 后面的操作
volatile 规则volatile 写操作 happens-before 后续的 volatile 读操作
锁规则unlock 操作 happens-before 后续的 lock 操作
传递性如果 A happens-before B,B happens-before C,则 A happens-before C
线程启动规则Thread.start() happens-before 线程中的任何操作
线程终止规则线程中的任何操作 happens-before Thread.join()
对象终结规则对象构造完成 happens-before 调用 finalize()

happens-before 示例

public class HappensBeforeDemo {
    private volatile int a = 0;
    private int b = 0;
    
    public void writer() {
        a = 1;              // 操作1
        b = 2;              // 操作2
        // 根据程序顺序规则:操作1 happens-before 操作2
        // 根据 volatile 规则:操作1(volatile写)happens-before 后续的 volatile读
    }
    
    public void reader() {
        int r1 = a;         // 操作3(volatile读)
        int r2 = b;         // 操作4
        // 根据 volatile 规则:操作1 happens-before 操作3
        // 根据传递性:操作1 happens-before 操作4
        // 所以 r1 = 1 时,r2 一定等于 2
    }
}

五、并发安全的保障机制

使用 synchronized

public class SynchronizedCounter {
    private int count = 0;
    
    public synchronized void increment() {
        count++;
    }
    
    public synchronized int getCount() {
        return count;
    }
}

使用 Lock 接口

public class LockCounter {
    private int count = 0;
    private final Lock lock = new ReentrantLock();
    
    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }
    
    public int getCount() {
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }
}

使用原子类

public class AtomicCounter {
    private final AtomicInteger count = new AtomicInteger(0);
    
    public void increment() {
        count.incrementAndGet();
    }
    
    public int getCount() {
        return count.get();
    }
}

使用 ThreadLocal

public class ThreadLocalCounter {
    private static final ThreadLocal<Integer> counter = 
        ThreadLocal.withInitial(() -> 0);
    
    public void increment() {
        counter.set(counter.get() + 1);
    }
    
    public int getCount() {
        return counter.get();
    }
}

六、总结

Java 内存模型是并发编程的基础:

  1. 内存结构:主内存和工作内存,变量在两者之间传递
  2. volatile:保证可见性和有序性,不保证原子性
  3. 三大特性:原子性、可见性、有序性
  4. happens-before:定义操作之间的顺序关系
  5. 并发安全:通过 synchronized、Lock、原子类等实现

理解 JMM 的核心概念,对于编写线程安全的代码至关重要。在实际开发中,需要根据具体场景选择合适的并发控制方式。

© 版权声明
THE END
喜欢就支持一下吧
点赞12
相关推荐
评论 抢沙发

请登录后发表评论

    请登录后查看评论内容

温馨提示:
1、本内容转载于网络,版权归原作者所有!
2、本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
3、本内容若侵犯到你的版权利益,请联系我们,会尽快给予删除处理!