Java的volatile到底该如何理解?(上)

简介: Java的volatile到底该如何理解?(上)

volatile 的实现维度

级别 实现
Java 代码 volatile int i
ByteCode 字节码 ACC_VOLATILE
JVM 虚拟机规范 JVM 内存屏障
HotSpot 实现 汇编语言调用
CPU 级别 MESI 原语支持总线锁

可见性问题

让一个线程对共享变量的修改,能够及时的被其他线程看到。

根据JMM中规定的happen before和同步原则:

对某个volatile字段的写操作happens- before每个后续对该volatile字段的读操作。

对volatile变量v的写入,与所有其他线程后续对v的读同步


要满足这些条件,所以volatile关键字就有这些功能:


禁止缓存;

volatile变量的访问控制符会加个ACC_VOLATILE

对volatile变 量相关的指令不做重排序

volatile 变量可以被看作是一种 "轻量的 synchronized,可算是JVM提供的最轻量级的同步机制。


当一个变量定义为volatile后,可以保证此变量对所有线程的可见性。

原子性(Atomicity)

一次只允许一个线程持有某锁,一次只有一个线程能使用共享数据

由JMM直接保证的原子性变量操作包括read、load、use、assign、store和write六个,大致可以认为基础数据类型的访问读写是原子性的


如果应用场景需要一个更大范围的原子性保证,JMM还提供了lock和unlock操作来满足这种需求,尽管虚拟机未把lock与unlock操作直接开放给用户使用,但是却提供了更高层次的字节码指令monitorenter和monitorexit来隐匿地使用这两个操作,这两个字节码指令反映到Java代码中就是同步块synchronized关键字,因此在synchronized块之间的操作也具备原子性

可见性(Visibility)

当一个线程修改了线程共享变量的值,其它线程能够立即得知这个修改。

由于现代可共享内存的多处理器架构可能导致一个线程无法马上看到另一个线程操作产生的结果。所以 Java 内存模型规定了 JVM 的一种最小保证:什么时候写入一个变量对其他线程可见。


在现代可共享内存的多处理器体系结构中每个处理器都有自己的缓存,并周期性的与主内存协调一致。假设线程 A 写入一个变量值 V,随后另一个线程 B 读取变量 V 的值

在下列情况下,线程 B 读取的值可能不是线程 A 写入的最新值:


执行线程 A 的处理器把变量 V 缓存到寄存器中。

执行线程 A 的处理器把变量 V 缓存到自己的缓存中,但还没有同步刷新到主内存中去。

执行线程 B 的处理器的缓存中有变量 V 的旧值。

JMM通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式实现可见性,无论是普通变量还是volatile变量都是如此。


普通变量与volatile变量的区别:

volatile保证新值能立即同步到主内存,以及每使用前立即从内存刷新。

因此volatile保证了线程操作时变量的可见性,而普通变量则不保证。


除了volatile,Java还有两个关键字能实现可见性:

synchronized

由“对一个变量执行unlock前,必须先把此变量同步回主内存中(执行storewrite)”这条规则获得的

final

被final修饰的字段,在构造器一旦初始化完成,并且构造器没有把this引用传递出去(this引用逃逸是一件危险事情,其它线程可能通过该引用访问到初始化了一半的对象),那在其他线程中就能看见final字段值。

final在该对象的构造器设置对象的字段,当线程看到该对象时,将始终看到该对象的final字段的正确构造版本。

伪代码示例:

f = new finalDemo();

读取到的 f.x 一定最新,x为final字段。

若在构造器设置字段后发生读取,则会看到该final字段分配的值,否则它将看到默认值;

伪代码示例:

public finalDemo() {
  x=1;
  y=x;
};

y会等于1。

读取该共享对象的final成员变量之前,先要读取共享对象。

伪代码示例:

r = new ReferenceObj(); 
k = r.f; 

这两个操作不能重排序

通常static final是不可修改的字段,然而System.in、System.out和System.err 都是static final字段,遗留原因,必须允许通过set方法改变,我们将这些字段称为写保护,以区别于普通final字段:

9.png

10.png

11.png

12.png

必须确保释放锁之前对共享数据做出的更改,对于随后获得该锁的另一个线程可见,对域中的值做赋值和返回的操作通常是原子性的,但递增/减并不是。


volatile对所有线程立即可见,对volatile变量所有的写操作都能立即返回到其它线程,即volatile变量在各个线程中是一致的,但并非基于volatile变量的运算在并发下是安全的。


volatile变量在各线程的工作内存中不存在一致性问题(在各个线程的工作内存中volatile变量可存在不一致,但由于每次使用前都要先刷新,执行引擎看不到不一致的情况,因此可认为不存在一致性问题),但Java里的运算并非原子操作,导致volatile变量的运算在并发下一样不安全:

public class Atomicity {
  int i;
  void f(){
    i++;
  }
  void g(){
    i += 3;
  }
}

编译后文件

void f();
    0  aload_0 [this]
    1  dup
    2  getfield concurrency.Atomicity.i : int [17]
    5  iconst_1
    6  iadd
    7  putfield concurrency.Atomicity.i : int [17]
 // Method descriptor #8 ()V
 // Stack: 3, Locals: 1
 void g();
    0  aload_0 [this]
    1  dup
    2  getfield concurrency.Atomicity.i : int [17]
    5  iconst_3
    6  iadd
    7  putfield concurrency.Atomicity.i : int [17]
}

每个操作都产生了一个 get 和 put ,之间还有一些其它指令。因此在获取和修改之间,另一个线程可能会修改这个域。所以,这些操作不是原子性的。

再看下面这个例子是否符合上面的描述:

public class AtomicityTest implements Runnable {
    private int i = 0;
    public int getValue() {
      return i;
    }
    private synchronized void evenIncrement() {
      i++;
      i++;
    }
    public void run() {
      while(true)
        evenIncrement();
    }
    public static void main(String[] args) {
      ExecutorService exec = Executors.newCachedThreadPool();
      AtomicityTest at = new AtomicityTest();
      exec.execute(at);
      while(true) {
        int val = at.getValue();
        if(val % 2 != 0) {
          System.out.println(val);
          System.exit(0);
        }
      }
    }
}
output:
1

该程序将找到奇数值并终止。尽管return i原子性,但缺少同步使得其数值可以在处于不稳定的中间状态时被读取。由于 i 不是 volatile,存在可见性问题

getValue() 和 evenIncrement() 必须synchronized。


对于基本类型的读/写操作被认为是安全的原子性操作

但当对象处于不稳定状态时,仍旧很有可能使用原子性操作来访问他们

最明智的做法是遵循同步的规则


volatile 变量只保证可见性

在不符合以下条件规则的运算场景中,仍需加锁(使用synchronized或JUC原子类)保证原子性:


运算结果不依赖变量的当前值,或者能确保只有单一的线程修改变量的值

变量不需要与其它的状态变量共同参与不可变类约束

基本上,若一个域可能会被多个任务同时访问or这些任务中至少有一个是写任务,那就该将此域设为volatile

当一个域定义为 volatile 后,将具备


1.保证此变量对所有的线程的可见性,当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,其它线程每次使用前立即从主内存刷新

但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存来完成

2.禁止指令重排序。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置)

这些操作的目的是用线程中的局部变量维护对该域的精确同步

目录
相关文章
|
7月前
|
存储 缓存 Java
【高薪程序员必看】万字长文拆解Java并发编程!(5):深入理解JMM:Java内存模型的三大特性与volatile底层原理
JMM,Java Memory Model,Java内存模型,定义了主内存,工作内存,确保Java在不同平台上的正确运行主内存Main Memory:所有线程共享的内存区域,所有的变量都存储在主存中工作内存Working Memory:每个线程拥有自己的工作内存,用于保存变量的副本.线程执行过程中先将主内存中的变量读到工作内存中,对变量进行操作之后再将变量写入主内存,jvm概念说明主内存所有线程共享的内存区域,存储原始变量(堆内存中的对象实例和静态变量)工作内存。
242 0
|
9月前
|
设计模式 存储 SQL
【Java并发】【volatile】适合初学者体质的volatile
当你阅读dalao的框架源码的时候,你是否会见到这样一个关键字 - - - volatie,诶,你是否会好奇,为什么要加它?加了它有什么作用?
239 14
【Java并发】【volatile】适合初学者体质的volatile
|
9月前
|
存储 缓存 安全
【原理】【Java并发】【volatile】适合初学者体质的volatile原理
欢迎来到我的技术博客!我是一名热爱编程的开发者,梦想是写出高端的CRUD应用。2025年,我正在沉淀自己,博客更新速度也在加快。在这里,我会分享关于Java并发编程的深入理解,尤其是volatile关键字的底层原理。 本文将带你深入了解Java内存模型(JMM),解释volatile如何通过内存屏障和缓存一致性协议确保可见性和有序性,同时探讨其局限性及优化方案。欢迎订阅专栏《在2B工作中寻求并发是否搞错了什么》,一起探索并发编程的奥秘! 关注我,点赞、收藏、评论,跟上更新节奏,让我们共同进步!
387 8
【原理】【Java并发】【volatile】适合初学者体质的volatile原理
|
10月前
|
缓存 安全 Java
Volatile关键字与Java原子性的迷宫之旅
通过合理使用 `volatile`和原子操作,可以在提升程序性能的同时,确保程序的正确性和线程安全性。希望本文能帮助您更好地理解和应用这些并发编程中的关键概念。
262 21
|
存储 SQL 缓存
揭秘Java并发核心:深度剖析Java内存模型(JMM)与Volatile关键字的魔法底层,让你的多线程应用无懈可击
【8月更文挑战第4天】Java内存模型(JMM)是Java并发的核心,定义了多线程环境中变量的访问规则,确保原子性、可见性和有序性。JMM区分了主内存与工作内存,以提高性能但可能引入可见性问题。Volatile关键字确保变量的可见性和有序性,其作用于读写操作中插入内存屏障,避免缓存一致性问题。例如,在DCL单例模式中使用Volatile确保实例化过程的可见性。Volatile依赖内存屏障和缓存一致性协议,但不保证原子性,需与其他同步机制配合使用以构建安全的并发程序。
253 0
|
12月前
|
存储 缓存 Java
Java 并发编程——volatile 关键字解析
本文介绍了Java线程中的`volatile`关键字及其与`synchronized`锁的区别。`volatile`保证了变量的可见性和一定的有序性,但不能保证原子性。它通过内存屏障实现,避免指令重排序,确保线程间数据一致。相比`synchronized`,`volatile`性能更优,适用于简单状态标记和某些特定场景,如单例模式中的双重检查锁定。文中还解释了Java内存模型的基本概念,包括主内存、工作内存及并发编程中的原子性、可见性和有序性。
324 5
Java 并发编程——volatile 关键字解析
|
缓存 安全 算法
Java面试题:如何通过JVM参数调整GC行为以优化应用性能?如何使用synchronized和volatile关键字解决并发问题?如何使用ConcurrentHashMap实现线程安全的缓存?
Java面试题:如何通过JVM参数调整GC行为以优化应用性能?如何使用synchronized和volatile关键字解决并发问题?如何使用ConcurrentHashMap实现线程安全的缓存?
188 0
|
12月前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
292 7
|
12月前
|
缓存 安全 Java
Java volatile关键字:你真的懂了吗?
`volatile` 是 Java 中的轻量级同步机制,主要用于保证多线程环境下共享变量的可见性和防止指令重排。它确保一个线程对 `volatile` 变量的修改能立即被其他线程看到,但不能保证原子性。典型应用场景包括状态标记、双重检查锁定和安全发布对象等。`volatile` 适用于布尔型、字节型等简单类型及引用类型,不适用于 `long` 和 `double` 类型。与 `synchronized` 不同,`volatile` 不提供互斥性,因此在需要互斥的场景下不能替代 `synchronized`。
3438 3
|
缓存 Java 编译器
JAVA并发编程volatile核心原理
volatile是轻量级的并发解决方案,volatile修饰的变量,在多线程并发读写场景下,可以保证变量的可见性和有序性,具体是如何实现可见性和有序性。以及volatile缺点是什么?