前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java并发——volatile(十一)

Java并发——volatile(十一)

原创
作者头像
翰墨飘香
发布2024-07-04 23:32:30
840
发布2024-07-04 23:32:30
举报
文章被收录于专栏:Java并发

一、volatile 是什么

volatile是 Java 的一个关键字,是一种同步机制。volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。

当某个变量是共享变量,且这个变量是被 volatile 修饰的,那么在修改了这个变量的值之后,再读取该变量的值时,可以保证获取到的是修改后的最新的值,而不是过期的值。

二、Java内存模型(JMM)

Java Memory Model(简称JMM)Java内存模型(注意和JVM内存模型区别),本身是一种抽象的概念,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。

参考 玩命死磕Java内存模型(JMM)与Volatile关键字底层原理

https://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#whatismm

https://juejin.cn/post/6844903520760496141?searchId=2024061923154431D1C78C4D7EDB8805F0

2.1 计算机硬件内存架构

由于 CPU 的处理速度很快,相比之下,内存的速度就显得很慢,所以为了提高 CPU 的整体运行效率,减少空闲时间,在 CPU 和内存之间会有 cache 层,也就是缓存层的存在。虽然缓存的容量比内存小,但是缓存的速度却比内存的速度要快得多。

线程间对于共享变量的可见性问题,并不是直接由多核引起的,而是由多级缓存引起的:每个核心在获取数据时,都会将数据从内存一层层往上读取,同样,后续对于数据的修改也是先写入到自己的 L1 缓存中,然后等待时机再逐层往下同步,直到最终刷回内存。

2.1 主内存(Main Memory)

  • 定义:主内存是Java内存模型中的一个抽象概念,它代表了所有变量都存储的共享内存区域。主内存可以被多个线程同时访问。
  • 内容:主内存存放了程序中所有的类实例、静态数据等变量。这些变量是线程共享的,即多个线程都可以访问和修改主内存中的变量。不管是类的成员变量、还是方法中的局部变量,又或者共享的类信息、常量、静态变量等数据,包括所有线程创建的实例对象,都会被存放在主内存中(除开栈上分配的对象)。
  • 作用:主内存是线程间通信的桥梁,线程间变量值的传递都需要通过主内存来完成。

2.2 工作内存(Working Memory)

  • 定义:工作内存是Java内存模型中为每个线程分配的一个私有内存区域,用于存储线程私有的数据。每个线程都有一个独立的工作内存。应该包括程序计数器、虚拟机栈以及本地方法栈。
  • 内容:工作内存中存储了当前方法的所有本地变量信息及主内存中变量的副本,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的数据。主要存储每个线程的工作内存对其它线程不可见,比如T1的工作内存中,存储着主内存中拷贝回来的某个共享变量副本,这对于T2线程也是不可见的。 就算是两个线程执行的是同一段代码、同一个方法,它们也只会在各自的工作内存中,创建属于当前线程的本地变量、字节码行号指示器、相关Native方法等信息,而不会两者之间共用一块内存的数据。 注意:由于工作内存是每个线程的私有数据,线程间无法相互访问工作内存,线程之间的通讯需要依赖于主存,因此存储在工作内存的数据不存在线程安全问题。

2.3 工作内存与主内存的关系

JMM 有以下规定:

(1)所有的变量都存储在主内存中,同时每个线程拥有自己独立的工作内存,而工作内存中的变量的内容是主内存中该变量的拷贝;

(2)线程不能直接读 / 写主内存中的变量,但可以操作自己工作内存中的变量,然后再同步到主内存中,这样,其他线程就可以看到本次修改;

(3) 主内存是由多个线程所共享的,但线程间不共享各自的工作内存,如果线程间需要通信,则必须借助主内存中转来完成。

三、volitale作用

面试官没想到一个Volatile,我都能跟他扯半小时

彻底理解volatile

3.1 volitale主要作用

保证内存线程可见(保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。)Happens-before 关系中对于 volatile 是这样描述的:对一个 volatile 变量的写操作 happen-before 后面对该变量的读操作。

这就代表了如果变量被 volatile 修饰,那么每次修改之后,接下来在读取这个变量的时候一定能读取到该变量最新的值。

保证内存有序性(防止指令重排序) 先介绍一下 as-if-serial语义:不管怎么重排序,(单线程)程序的执行结果不会改变。在满足 as-if-serial 语义的前提下,由于编译器或 CPU 的优化,代码的实际执行顺序可能与我们编写的顺序是不同的,这在单线程的情况下是没问题的,但是一旦引入多线程,这种乱序就可能会导致严重的线程安全问题。用了 volatile 关键字就可以在一定程度上禁止这种重排序。

3.2 volatile解决内存可见性

怎么做?

volatile修饰共享变量

什么原理?

每个线程操作数据的时候会把数据从主内存读取到自己的工作内存,如果他操作了数据并且写会了,他其他已经读取的线程的变量副本就会失效了,需要都数据进行操作又要再次去主内存中读取了。

volatile保证不同线程对共享变量操作的可见性,也就是说一个线程修改了volatile修饰的变量,当修改写回主内存时,另外一个线程立即看到最新的值。

3.3 volatile解决内存有序性

什么是重排序?

为了提高性能,编译器和处理器常常会对既定的代码执行顺序进行指令重排序

as-if-serial语义

不管怎么重排序,(单线程)程序的执行结果不会改变。在满足 as-if-serial 语义的前提下,由于编译器或 CPU 的优化,代码的实际执行顺序可能与我们编写的顺序是不同的,这在单线程的情况下是没问题的,但是一旦引入多线程,这种乱序就可能会导致严重的线程安全问题。

原理是什么?——内存屏障

volitale使用了内存屏障保证不会执行重排序。

JMM内存屏障
volatile重排序规则表

java编译器会在生成指令系列时在适当的位置会插入内存屏障指令来禁止特定类型的处理器重排序。为了实现volatile的内存语义,JMM会限制特定类型的编译器和处理器重排序,JMM会针对编译器制定volatile重排序规则表:

"NO"表示禁止重排序。为了实现volatile内存语义时,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎是不可能的,为此,JMM采取了保守策略:

在每个volatile写操作的前面插入一个StoreStore屏障;

在每个volatile写操作的后面插入一个StoreLoad屏障;

在每个volatile读操作的后面插入一个LoadLoad屏障;

在每个volatile读操作的后面插入一个LoadStore屏障。

需要注意的是:volatile写是在前面和后面分别插入内存屏障,而volatile读操作是在后面插入两个内存屏障

StoreStore屏障:禁止上面的普通写和下面的volatile写重排序;

StoreLoad屏障:防止上面的volatile写与下面可能有的volatile读/写重排序

LoadLoad屏障:禁止下面所有的普通读操作和上面的volatile读重排序

LoadStore屏障:禁止下面所有的普通写操作和上面的volatile读重排序

四、happens-before规则

如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。

volatile域规则:对一个volatile域的写操作,happens-before于任意线程后续对这个volatile域的读。

如果现在我的变了falg变成了false,那么后面的那个操作,一定要知道我变了。

聊了这么多,我们要知道Volatile是没办法保证原子性的,一定要保证原子性,可以使用其他方法。

五、volitale和synchronized对比

相比于 synchronized 或者 Lock,volatile 是更轻量的,因为使用 volatile 不会发生上下文切换等开销很大的情况,不会让线程阻塞。但正是由于它的开销相对比较小,所以它的效果,也就是能力,相对也小一些。

相似性:volatile 可以看作是一个轻量版的 synchronized,比如一个共享变量如果自始至终只被各个线程赋值和读取,而没有其他操作的话,那么就可以用 volatile 来代替 synchronized 或者代替原子变量,足以保证线程安全。实际上,对 volatile 字段的每次读取或写入都类似于“半同步”——读取 volatile 与获取 synchronized 锁有相同的内存语义,而写入 volatile 与释放 synchronized 锁具有相同的语义。

不可代替:但是在更多的情况下,volatile 是不能代替 synchronized 的,volatile 并没有提供原子性和互斥性。

性能方面:volatile 属性的读写操作都是无锁的,正是因为无锁,所以不需要花费时间在获取锁和释放锁上,所以说它是高性能的,比 synchronized 性能更好。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、volatile 是什么
  • 二、Java内存模型(JMM)
    • 2.1 计算机硬件内存架构
      • 2.1 主内存(Main Memory)
        • 2.2 工作内存(Working Memory)
          • 2.3 工作内存与主内存的关系
          • 三、volitale作用
            • 3.1 volitale主要作用
              • 3.2 volatile解决内存可见性
                • 怎么做?
                • 什么原理?
              • 3.3 volatile解决内存有序性
                • 什么是重排序?
                • as-if-serial语义
                • 原理是什么?——内存屏障
            • 四、happens-before规则
            • 五、volitale和synchronized对比
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档