Java系列-valitile

news2024/12/28 21:01:32

背景

        volatile这个关键字可以说是面试过程中出现频率最高的一个知识点了,面试官的问题也是五花八门,各种刁钻的角度。之前也是简单背过几道八股文,什么可见性,防止指令重拍等,但面试官一句:volatile原理是什么,其他的线程是如果判断栈内存的值不可信的。Java面试好卷啊,然后也是查漏补缺吧。

在整理资料的过程中也收集了几个常见的面试题

volatile关键字的作用是什么?

volatile能保证原子性吗?

i++为什么不能保证原子性?

之前32位机器上共享的long和double变量的为什么要用volatile? 现在64位机器上是否也要设置呢?

volatile是如何实现可见性的? 内存屏障。

volatile是如何实现有序性的? happens-before等

说下volatile的应用场景?


著作权归@pdai所有 原文链接:https://pdai.tech/md/Java/thread/Java-thread-x-key-volatile.html

        volatile关键字通常被比喻为”轻量级的synchronized’,与synchronized不同的是,它是一个变量修饰待,只能用来修饰变量,无法修饰方法及代码块等。        

        Java中有两种自带的同步机制:锁机制(如synchronized,Lock)和volatile变量。相对于synchronized为代表的锁机制,volatile则更加轻量,因为它不会涉及线程上下文的切换。它的作用有以下两点:

保证可见性

      正如前面所写,当一个变量被volatile修饰时,如果某个对这个变量进行写操作,该线程会将此共享变量更新到主内存中,其他线程也会从主内存中读取该变量。通过这样,从而保证该变量在各个线程中的可见性。

保证有序性

JVM为了优化性能,会进行指令重排。对于单线程而言,即使有指令重排,也一定可以保证结果的正确性。可以在多线程下,指令重排就可能影响最终的结果。通过禁止指令重排来保证多线程下的有序性。

原理

保证可见性和有序性 

        在多线程环境下,一个线程对共享变量的操作,其他线程是不可见的,往往就会导致线程安全的问题。

        Java提供了volatile来保证可见性,当一个变量被volatile修饰后,表示着线程本地内存无效,需要从主内存中获取。当一个线程修改共享变量后,共享变量会立即被更新到主内存中,其他线程读取共享变量时,会直接从主内存中读取。当然,synchronize和Lock都可以保证可见性。synchronized和LocK能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

        跟锁机制来保证可见性不同的是,volatile是基于内存屏障(Memory Barrier)实现可见性的。

        在程序运行时,为了提高执行性能,编译器和处理器会对指令进行重排序,JMM 为了保证在不同的编译器和 CPU 上有相同的结果,通过插入特定类型的内存屏障来禁止特定类型的编译器重排序和处理器重排序。插入一条内存屏障会告诉编译器和 CPU:不管什么指令都不能和这条 Memory Barrier 指令重排序。

通过 hsdis 和 jitwatch 工具可以得到编译后的汇编代码:

  0x000000000295158c: lock cmpxchg %rdi,(%rdx)  //在 volatile 修饰的共享变量进行写操作的时候会多出 lock 前缀的指令

被volatile修改的变量进行写操作时会多出来lock 前缀指令。对于操作系统和CPU处理器遇到lock指令会做两件事:

  • 将当前处理器(CPU)缓存(寄存器)的数据写回到主内存。

  • 使在其他处理器里缓存了该内存地址的数据无效。

        通过lock指令保证了内存一致性,从而实现了多线程下的共享变量的可见性。这里需要补充一下关于内存的知识。思考:Java内存模型和硬件内存模型-CSDN博客

        此外内存屏障也能保证有序性。(一般都是说是happens-before 关系,其实本质还是内存屏障)。当Java在指令重排序时,不会把后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;也即是,在执行到内存屏障这个指令时,在他前面的操作已经完成。

具体来说,寄存器和内存之间数据数据交互有两个指令

Load

用于把内存中的数据装载到寄存器中。--- 读操作(对内存而言)

Store

用于把寄存器中的数据存入内存。 --- 写操作(对内存而言)

通过这个两个指令,Java可以实现了四种内存屏障,完成一系列的屏障和数据同步功能

LoadLoad

Load1; LoadLoad; Load2

在Load2及后续读操作执行前,保证Load1要读取完毕。

StoreStore

Store1; StoreStore; Store2,

在Store2及后续写操作执行前,保证Store1的写操作对其它处理器可见。

LoadStore

Load1; LoadStore; Store2,

在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。

StoreLoad

Store1; StoreLoad; Load2,

 在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能

volatile的内存屏障策略非常严格保守,非常悲观且毫无安全感的心态:

在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障;
在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障;

举个例子,如下图所示,在store1指令和store5指令之间加上StoreStore屏障。通过StoreStore屏障将指令分成了区域1和区域2,。无论指令如何重排序,Store1总是会先于store5执行。StoreLoad同理。

由于内存屏障的作用,避免了volatile变量和其它指令重排序、线程之间实现了通信,使得volatile表现出了锁的特性。

保证单次读写的原子性

这里涉及到了两个面试题

问题一:i++为什么不能保证原子性?

对于编译器和操作系统而言,i++是读,写两次操作。并不是单次读写。包含三步骤

1. 从内存中读取i的值

2. 对i加1

3. 将i的值写回内存

当并发量大的情况下,多个线程同时进行i++的时候,会出现同时从内存中读取到同一个值,然后进行运算,从而出现不符合预期的结果。所以volatile是无法保证复合操作的原子性,可以通过使用线程安全类或者加锁的方式来保证+1操作的原子性。

问题二:之前32位机器上共享的long和double变量的为什么要用volatile? 现在64位机器上是否也要设置呢?

long和double是64位的,在32位的jdk中完成write操作是需要两次操作的(每次执行32位)。也就是long和double的write操作是非原子性的。非原子的操作在多线程环境下会有线程安全问题。

比如A,B两个线程同时的去修改long类型x的值,可能x的高32位是A设置的,低32位是B设置的,导致结果不是程序想要的。因此,将共享long和double变量设置为volatile类型能保证任何情况下单次读/写操作的原子性。

但是最新JDK实现还是实现了原子操作的。

应用场景

根据volatile的原理和作用,找了以下几个应用场景

场景1:状态标志

也许实现 volatile 变量的规范使用仅仅是使用一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或请求停机。

volatile boolean shutdownRequested;
......
public void shutdown() { shutdownRequested = true; }
public void doWork() { 
    while (!shutdownRequested) { 
        // do stuff
    }
}

场景2:开销较低的读-写锁策略

volatile 的功能还不足以实现计数器。因为 ++x 实际上是三种操作(读、添加、存储)的简单组合,如果多个线程凑巧试图同时对 volatile 计数器执行增量操作,那么它的更新值有可能会丢失。 如果读操作远远超过写操作,可以结合使用内部锁和 volatile 变量来减少公共代码路径的开销。 安全的计数器使用 synchronized 确保增量操作是原子的,并使用 volatile 保证当前结果的可见性。如果更新不频繁的话,该方法可实现更好的性能,因为读路径的开销仅仅涉及 volatile 读操作,这通常要优于一个无竞争的锁获取的开销。

@ThreadSafe
public class CheesyCounter {
    // Employs the cheap read-write lock trick
    // All mutative operations MUST be done with the 'this' lock held
    @GuardedBy("this") private volatile int value;
 
    public int getValue() { return value; }
 
    public synchronized int increment() {
        return value++;
    }
}

场景3:双重检查(double-checked)

就是我们上文举的例子。

单例模式的一种实现方式,但很多人会忽略 volatile 关键字,因为没有该关键字,程序也可以很好的运行,只不过代码的稳定性总不是 100%,说不定在未来的某个时刻,隐藏的 bug 就出来了。

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

资料

https://pdai.tech/md/Java/thread/Java-thread-x-key-volatile.html

Java volatile关键字最全总结:原理剖析与实例讲解(简单易懂)-CSDN博客

https://zhuanlan.zhihu.com/p/35386457

https://zhuanlan.zhihu.com/p/43526907

https://pdai.tech/md/Java/jvm/Java-jvm-struct.html

https://juejin.cn/post/7125709539596304420

https://www.jianshu.com/p/ef8de88b1343

为何在volatile写 之前加storestore内存屏障即可,不需要 loadstore么?_jvm volatile写操作前为什么不插入loadstore屏障-CSDN博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1912724.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Vue基础--v-model/v-for/事件属性/侦听器

目录 一 v-model表单元素 1.1 v-model绑定文本域的value 1.1.1 lazy属性:光标离开再发请求 1.1.2 number属性:如果能转成number就会转成numer类型 1.1.3 trim属性:去文本域输入的前后空格 1.2v-model绑定单选checkbox 1.3代码展示 二 …

Python OpenCV 教学取得视频资讯

这篇教学会介绍使用OpenCV,取得影像的长宽尺寸、以及读取影像中某些像素的颜色数值。 因为程式中的OpenCV 会需要使用镜头或GPU,所以请使用本机环境( 参考:使用Python 虚拟环境) 或使用Anaconda Jupyter 进行实作( 参考:使用Anaco…

基于单片机的温湿度感应智能晾衣杆系统设计

[摘 要] 本设计拟开发一种湿度感应智能晾衣杆系统 , 此新型晾衣杆是以单片机为主控芯片 来控制的实时检测系统 . 该系统使用 DHT11 温湿度传感器来检测大气的温湿度 , 然后通过单 片机处理信息来控制 28BYJ &…

配置路由器支持Telnet操作 计网实验

实验要求: 假设某学校的网络管理员第一次在设备机房对路由器进行了初次配置后,他希望以后在办公室或出差时也可以对设备进行远程管理,现要在路由器上做适当配置,使他可以实现这一愿望。 本实验以一台R2624路由器为例,…

使用 Hugging Face 的 Transformers 库加载预训练模型遇到的问题

题意: Size mismatch for embed_out.weight: copying a param with shape torch.Size([0]) from checkpoint - Huggingface PyTorch 这个错误信息 "Size mismatch for embed_out.weight: copying a param with shape torch.Size([0]) from checkpoint - Hugg…

Redis管理禁用命令

在redis数据量比较大时,执行 keys * ,fluashdb 这些命令,会导致redis长时间阻塞,大量请求被阻塞,cpu飙升,严重可能导致redis宕机,数据库雪崩。所以一些命令在生产环境禁止使用。 Redis 禁用命令…

开始尝试从0写一个项目--前端(二)

修改请求路径的位置 将后续以及之前的所有请求全都放在同一个文件夹里面 定义axios全局拦截器 为了后端每次请求都需要向后端传递jwt令牌检验 ps:愁死了,翻阅各种资料,可算是搞定了,哭死~~ src\utils\request.js import axio…

【QML之·基础语法概述】

系列文章目录 文章目录 前言一、QML基础语法二、属性三、脚本四、核心元素类型4.1 元素可以分为视觉元素和非视觉元素。4.2 Item4.2.1 几何属性(Geometry):4.2.2 布局处理:4.2.3 键处理:4.2.4 变换4.2.5 视觉4.2.6 状态定义 4.3 Rectangle4.3.1 颜色 4.4…

互联网3.0时代的变革者:华贝甄选大模型创新之道

在当今竞争激烈的商业世界中,华贝甄选犹如一颗璀璨的明星,闪耀着独特的光芒。 华贝甄选始终将技术创新与研发视为发展的核心驱动力。拥有先进的研发团队和一流设施,积极探索人工智能、大数据、区块链等前沿技术,为用户提供高性能…

Knife4j的介绍与使用

目录 一、简单介绍1.1 简介1.2 主要特点和功能: 二、使用步骤:2.1 添加依赖:2.2 yml数据源配置2.3 创建knife4j配置类2.4 注解的作用 最后 一、简单介绍 1.1 简介 Knife4j 是一款基于Swagger的开源文档管理工具,主要用于生成和管…

【PTA天梯赛】L1-003 个位数统计(15分)

作者:指针不指南吗 专栏:算法刷题 🐾或许会很慢,但是不可以停下来🐾 文章目录 题目题解总结 题目 题目链接 题解 使用string把长度达1000位的数字存起来开一个代表个位数的数组 a[11]倒序计算最后一位,…

第16章 主成分分析:四个案例及课后习题

1.假设 x x x为 m m m 维随机变量,其均值为 μ \mu μ,协方差矩阵为 Σ \Sigma Σ。 考虑由 m m m维随机变量 x x x到 m m m维随机变量 y y y的线性变换 y i α i T x ∑ k 1 m α k i x k , i 1 , 2 , ⋯ , m y _ { i } \alpha _ { i } ^ { T } …

从微软 Word 中提取数据

从 Microsoft Word 文档中提取数据可以通过编程来实现,有几种常见的方法,其中之一是使用 Python 和 python-docx 库。python-docx 是一个处理 .docx 文件(Microsoft Word 文档)的 Python 库,可以读取和操作 Word 文档的…

泛微开发修炼之旅--36通过js控制明细表中同一列中多个浏览框的显示控制逻辑(明细表列中多字段显示逻辑控制)

文章链接:36通过js控制明细表中同一列中多个浏览框的显示控制逻辑(明细表列中多字段显示逻辑控制)

谷粒商城学习笔记-22-分布式组件-SpringCloud-OpenFeign测试远程调用

文章目录 一,OpenFeign的简介二,OpenFeign的使用步骤1,场景说明2,引入依赖2,开启OpenFeign3,编写Feign接口4,使用feign调用远程接口5,验证 错误记录 上一节学习了注册中心&#xff0…

变长输入神经网络设计

我对使用 PyTorch 可以轻松构建动态神经网络的想法很感兴趣,因此我决定尝试一下。 我脑海中的应用程序具有可变数量的相同类型的输入。对于可变数量的输入,已经使用了循环或递归神经网络。但是,这些结构在给定行的输入之间施加了一些顺序或层…

前端面试题31(TCP与UDP区别)

TCP (Transmission Control Protocol) 和 UDP (User Datagram Protocol) 是两种在网络通信中常用的传输层协议,它们在多个方面存在显著差异,主要体现在以下几个方面: 连接方式: TCP 是面向连接的协议。在数据传输开始之前&#xf…

STM32学习历程(day6)

EXTI外部中断使用教程 首先先看下EXTI的框图 看这个框图就能知道要先初始化GPIO外设 那么和前面一样 1、先RCC使能时钟 2、配置GPIO 选择端口为输入模式, 3、配置AFIO,选择我们用的GPIO连接到后面的EXTI 4、配置EXTI,选择边沿触发方式…

前端javascript中的排序算法之选择排序

选择排序(Selection Sort)基本思想: 是一种原址排序法; 将数组分为两个区间:左侧为已排序区间,右侧为未排序区间。每趟从未排序区间中选择一个值最小的元素,放到已排序区间的末尾,从…

从Helm到 Operator:Kubernetes应用管理的进化

🧰Helm 的作用 在开始前需要先对 kubernetes Operator 有个简单的认识。 以为我们在编写部署一些简单 Deployment 的时候只需要自己编写一个 yaml 文件然后 kubectl apply 即可。 apiVersion: apps/v1 kind: Deployment metadata: labels: app: k8s-combat …