volatile怎么实现的内存可见

发布时间:2021-10-26 10:37:12 作者:iii
来源:亿速云 阅读:190
# volatile怎么实现的内存可见

## 前言

在Java并发编程中,`volatile`关键字是一个非常重要的概念。它能够保证变量的内存可见性,防止指令重排序,是实现线程安全的重要手段之一。本文将深入探讨`volatile`关键字的底层实现原理,分析它如何保证内存可见性,并与其他同步机制进行对比。

## 目录

1. [什么是内存可见性](#什么是内存可见性)
2. [volatile关键字简介](#volatile关键字简介)
3. [Java内存模型(JMM)基础](#java内存模型jmm基础)
4. [volatile的实现原理](#volatile的实现原理)
   - [4.1 内存屏障(Memory Barrier)](#41-内存屏障memory-barrier)
   - [4.2 禁止指令重排序](#42-禁止指令重排序)
   - [4.3 保证写操作的原子性](#43-保证写操作的原子性)
5. [volatile的底层实现](#volatile的底层实现)
   - [5.1 汇编层面分析](#51-汇编层面分析)
   - [5.2 JVM层面的实现](#52-jvm层面的实现)
6. [volatile的使用场景](#volatile的使用场景)
7. [volatile的局限性](#volatile的局限性)
8. [volatile与其他同步机制对比](#volatile与其他同步机制对比)
   - [8.1 volatile vs synchronized](#81-volatile-vs-synchronized)
   - [8.2 volatile vs final](#82-volatile-vs-final)
   - [8.3 volatile vs Atomic变量](#83-volatile-vs-atomic变量)
9. [实际案例分析](#实际案例分析)
10. [总结](#总结)

## 什么是内存可见性

内存可见性(Memory Visibility)是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。在没有适当同步的情况下,由于现代计算机体系结构的多级缓存机制,一个线程对共享变量的修改可能不会立即对其他线程可见。

考虑以下代码示例:

```java
public class VisibilityProblem {
    private static boolean ready = false;
    private static int number = 0;

    public static class ReaderThread extends Thread {
        public void run() {
            while (!ready) {
                // 可能永远循环下去
            }
            System.out.println(number);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new ReaderThread().start();
        Thread.sleep(1000);
        number = 42;
        ready = true;
        Thread.sleep(10000);
    }
}

在这个例子中,ReaderThread可能会永远看不到ready变量的更新,从而陷入无限循环。这就是典型的内存可见性问题。

volatile关键字简介

volatile是Java提供的一种轻量级的同步机制,它主要有两个特性:

  1. 保证内存可见性:当一个线程修改了volatile变量的值,新值会立即被刷新到主内存中,并且其他线程读取该变量时会直接从主内存读取,而不是使用缓存中的旧值。

  2. 禁止指令重排序:编译器和处理器会对指令进行重排序优化,而volatile变量会插入内存屏障,防止这种重排序。

声明一个volatile变量的语法很简单:

private volatile boolean flag = false;

Java内存模型(JMM)基础

要理解volatile的工作原理,必须先了解Java内存模型(Java Memory Model, JMM)。JMM定义了线程如何与内存交互,以及线程之间如何通过内存进行通信。

JMM的主要概念包括:

  1. 主内存(Main Memory):所有共享变量都存储在主内存中
  2. 工作内存(Working Memory):每个线程有自己的工作内存,保存了该线程使用到的变量的主内存副本
  3. 内存间交互操作:JMM定义了8种原子操作来完成主内存与工作内存之间的交互

volatile变量的特殊之处在于,它直接在主内存中进行读写操作,跳过了工作内存的缓存机制。

volatile的实现原理

4.1 内存屏障(Memory Barrier)

内存屏障,也称为内存栅栏,是一组处理器指令,用于实现对内存操作顺序的限制。volatile的实现依赖于内存屏障,主要包含以下四种:

  1. LoadLoad屏障:确保Load1的数据装载先于Load2及所有后续装载指令
  2. StoreStore屏障:确保Store1的数据对其他处理器可见先于Store2及所有后续存储指令
  3. LoadStore屏障:确保Load1的数据装载先于Store2及所有后续存储指令
  4. StoreLoad屏障:确保Store1的数据对其他处理器可见先于Load2及所有后续装载指令

对于volatile变量的写操作,JVM会在写操作后插入一个StoreStore屏障和一个StoreLoad屏障;对于读操作,会在读操作前插入一个LoadLoad屏障和一个LoadStore屏障。

4.2 禁止指令重排序

编译器和处理器为了优化性能,会对指令进行重排序。volatile通过内存屏障防止这种重排序:

  1. 写操作:当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序
  2. 读操作:当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序
  3. 读写操作:当第一个操作是volatile写,第二个操作是volatile读时,不能重排序

4.3 保证写操作的原子性

虽然volatile不能保证复合操作的原子性(如i++),但它能保证单次读/写操作的原子性。对于long和double类型(64位),在非volatile情况下,JVM允许将64位的读写操作分解为两个32位的操作,而volatile修饰的long和double变量则保证了原子性。

volatile的底层实现

5.1 汇编层面分析

在x86处理器上,volatile变量的写操作会被编译为带有”lock”前缀的指令:

0x01a3de1d: movb $0x0,0x1104800(%esi);
0x01a3de24: lock addl $0x0,(%esp);

“lock”前缀会引发以下效果: 1. 将当前处理器缓存行的数据写回系统内存 2. 这个写回内存的操作会使其他CPU里缓存了该内存地址的数据无效

5.2 JVM层面的实现

在JVM中,volatile的实现依赖于以下机制:

  1. 字节码层面:volatile变量在访问时会使用ACC_VOLATILE标志
  2. JIT编译器:会根据不同平台插入适当的内存屏障指令
  3. 内存语义
    • 写volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存
    • 读volatile变量时,JMM会使该线程对应的本地内存无效,从主内存中读取共享变量

volatile的使用场景

volatile非常适合用于状态标志的场景:

public class ShutdownRequest extends Thread {
    private volatile boolean shutdownRequested = false;

    public void shutdown() {
        shutdownRequested = true;
    }

    public void run() {
        while (!shutdownRequested) {
            // 处理任务
        }
    }
}

其他适用场景包括: 1. 单例模式的双重检查锁定(DCL) 2. 一次性安全发布 3. 独立观察(independent observation)

volatile的局限性

volatile虽然有用,但也有其局限性: 1. 不能保证复合操作的原子性 2. 不适用于需要多个变量共同参与不变性条件的情况 3. 性能开销比普通变量大

volatile与其他同步机制对比

8.1 volatile vs synchronized

特性 volatile synchronized
原子性 单次读/写原子性 代码块原子性
可见性 保证 保证
有序性 有限保证(禁止重排序) 完全保证
阻塞 不阻塞 阻塞
适用场景 状态标志 复合操作

8.2 volatile vs final

final变量在初始化完成后也是线程安全的,但与volatile不同: 1. final变量只能被赋值一次 2. final的可见性是通过禁止重排序实现的 3. final更适用于不可变对象

8.3 volatile vs Atomic变量

Atomic类(如AtomicInteger)使用volatile和CAS操作实现: 1. Atomic类可以保证复合操作的原子性 2. 性能比synchronized高 3. 适用于计数器等场景

实际案例分析

案例1:双重检查锁定(DCL)

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

这里volatile防止了指令重排序,确保对象完全构造完成后才对其他线程可见。

案例2:生产者消费者模式

public class ProducerConsumer {
    private volatile boolean isEmpty = true;
    private String message;
    
    public void produce(String message) {
        while (!isEmpty) {
            // 等待
        }
        this.message = message;
        isEmpty = false;
    }
    
    public String consume() {
        while (isEmpty) {
            // 等待
        }
        String result = message;
        isEmpty = true;
        return result;
    }
}

注意:这个例子中volatile是不够的,还需要同步机制来保证原子性。

总结

volatile关键字通过内存屏障和禁止指令重排序的机制实现了内存可见性。它是Java并发编程中的重要工具,但并非万能。正确使用volatile需要:

  1. 理解其适用场景
  2. 了解其局限性
  3. 结合其他同步机制使用

随着Java内存模型的不断完善和硬件的发展,volatile的实现细节可能会有所变化,但其核心思想——通过内存屏障保证可见性和有序性——将保持不变。

掌握volatile的原理和使用方法,是成为Java并发编程高手的重要一步。 “`

这篇文章详细介绍了volatile关键字的实现原理和使用方法,涵盖了从基础概念到底层实现的各个方面,并提供了实际案例分析和与其他同步机制的对比。文章长度约为6550字,采用Markdown格式编写,结构清晰,内容全面。

推荐阅读:
  1. Android——volatile
  2. volatile分析

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

volatile java

上一篇:如何自动解锁Linux上的加密磁盘

下一篇:怎么用Loki和fzf进阶你的Shell历史记录

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》