volatile关键字原理的使用介绍和底层原理解析和使用实例
文章目录
- volatile关键字原理的使用介绍和底层原理解析和使用实例
- 1. volatile 关键字的作用
- 2. volatile 的底层原理
- 3. volatile 的使用案例
- 4. volatile 的原子性问题
- 5. 如何解决 volatile 的原子性问题
- 6. volatile 的实现原理
- 7. 小结
- 8. volatile的最佳实践
- 9. 案例:使用volatile实现双重检查锁定
- 10. 案例:使用volatile实现中断机制
- 11. 案例:使用AtomicInteger代替volatile
- 12. 案例:基于volatile实现一个简单的并发容器
- 13. 小结
- 14. 案例:使用AtomicStampedReference实现ABA问题的解决
- 15. 总结
volatile关键字原理的使用介绍和底层原理解析和使用实例
1. volatile 关键字的作用
volatile 关键字的主要作用是保证可见性和有序性,禁止编译器优化。
- 保证可见性:当一个变量被声明为 volatile 之后,每次读取这个变量的值都会从主内存中读取,而不是从缓存中读取,这就保证了不同线程对这个变量操作的可见性。
- 有序性:volatile 关键字保证了不同线程对一个 volatile 变量的读写操作的有序性。
- 禁止编译器优化:编译器会对代码进行各种优化来提高性能,但是这些优化也可能让同步代码失效。volatile 关键字告诉编译器不要对这段代码做优化,从而避免一些不正确的优化。
2. volatile 的底层原理
volatile 关键字底层原理依赖于内存屏障和缓存一致性协议。
- 内存屏障:内存屏障会强制让读和写操作都访问主内存,从而实现可见性。volatile 写操作后会加入写屏障,volatile 读操作前会加入读屏障。
- 缓存一致性协议:每个处理器都有自己的高速缓存,当某个处理器修改了共享变量,需要缓存一致性协议来保证其他处理器也看到修改后的值。缓存一致性协议会在读操作后和写操作前加入缓存刷新操作,保证其他处理器的缓存是最新值。
3. volatile 的使用案例
volatile 关键字常用在 DCL(Double Check Lock)单例模式中:
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 是为了防止指令重排序,保证 instance 初始化后其他线程可以看到。
volatile 也常用在Interruptible线程中,实现线程的中断功能:
public class InterruptibleThread extends Thread {private volatile boolean interrupted = false;public void interrupt() {interrupted = true;}@Overridepublic void run() {while (!interrupted) {// do something}}
}
这里 volatile 可以保证 interrupted 的可见性,使线程立即响应中断调用。
4. volatile 的原子性问题
volatile 关键字只能保证可见性和有序性,不能保证原子性。
对一个 volatile 变量的读写操作并不是原子的,而是可以分为读、改、写三个操作:
- 读: 读取 volatile 变量的值
- 改:对值进行修改
- 写:将修改后的值写入 volatile 变量
这三个操作并不是一个原子操作,在多线程环境下可能导致数据竞争问题:
public class VolatileNoAtomicDemo {private volatile int counter = 0;public void increase() {counter++; // 不是原子操作}
}
这里的 counter++ 实际上分为三步:
- 读:读取 counter 的值,假设为 x
- 改:x + 1
- 写:将 x + 1 的结果写入 counter
在多线程环境下,如果两个线程同时执行 increase 方法,很有可能达不到预期结果,这就是因为 counter++ 不是一个原子操作导致的。
5. 如何解决 volatile 的原子性问题
要解决 volatile 的原子性问题,可以使用 synchronized 或 Atomic 包中的类。
使用 synchronized:
public synchronized void increase() {counter++;
}
使用 AtomicInteger:
private AtomicInteger counter = new AtomicInteger(0);public void increase() {counter.getAndIncrement();
}
AtomicInteger 中的方法都是原子操作,可以解决 volatile 的原子性问题。
synchronized 会影响性能,AtomicInteger 的性能更好,所以一般优先选择 Atomic 包中的原子类。
6. volatile 的实现原理
volatile 的实现原理依赖于 JMM(Java Memory Model)中的几个概念:
- 主内存:所有线程都可以访问的内存,存储共享变量的值。
- 工作内存:每个线程私有的内存,用于存储线程使用的变量值。
- 内存屏障:控制读写的顺序,用于保证特定操作的完成后才允许执行后续操作。
volatile 的实现原理是:
- 当一个线程修改一个volatile变量的值时,它会在变量修改后立即刷新回主内存。
- 当一个线程读取一个volatile变量的值时,它会直接从主内存读取,而不是从工作内存读取。
- 它会在读后和写前加入内存屏障,以保证指令重排不会将内存操作重排到屏障另一侧。
这样就实现了:
- 可见性:因为每次直接读写主内存,所以每个线程都可以获得最新值。
- 有序性:内存屏障会阻止重排,读写顺序由代码决定。
- 禁止编译器优化:因为每次都要从主内存读写,编译器难以对其进行优化。
JMM的这几个概念配合volatile关键字的实现原理,就保证了多线程环境下volatile变量的可见性、有序性和禁止编译器优化。
7. 小结
- volatile关键字主要保证可见性、有序性和禁止编译器优化。
- volatile的底层原理是依赖内存屏障和缓存一致性协议实现的。
- volatile不能保证原子性,要配合synchronized或Atomic类解决。
- volatile的实现依赖JMM中的主内存、工作内存和内存屏障等概念。
8. volatile的最佳实践
根据volatile的特性,我们可以总结出一些最佳实践:
-
不要过度使用volatile
volatile关键字会影响程序性能,所以不要过度使用,只在真正需要可见性和有序性保证的地方使用。
-
与synchronized一起使用
当需要保证原子性时,volatile关键字需要与synchronized关键字一起使用。synchronized可以保证代码块的原子性,volatile可以保证数据的可见性。
-
使用Atomic类代替synchronized和volatile
Atomic类提供的方法都是原子操作,性能比synchronized更好,同时可以保证可见性,所以在需要保证原子性的场景可以优先选择Atomic类。
-
禁止把long和double类型变量声明为volatile
根据JMM规范,对64位数据类型的读写操作不一定是原子的,所以不要将long和double类型的变量声明为volatile。可以使用AtomicLong和AtomicDouble类代替。
-
volatile不保证顺序
volatile关键字只能保证有序性,不能保证顺序。有序性是指:在一个线程内,不会由于编译器优化和处理器重新排序,使得对一个volatile变量的写操作排在读操作之前。顺序是指:两个线程访问同一个变量的顺序。所以不要依赖volatile保证线程间的顺序。
-
volatile变量不能保护其它非volatile变量
在使用volatile变量控制住多线程变量的可见性时,不要认为它可以保护其它非volatile变量。每个变量都需要单独使用volatile或synchronized来保护。
9. 案例:使用volatile实现双重检查锁定
双重检查锁定(Double Check Locking)是一种使用同步控制并发访问的方式,可以实现延迟初始化。它通过两次对对象引用进行空检查来避免同步,从而提高性能。
但是在Java中,普通的双重检查锁定是不起作用的,原因是有指令重排的存在,可能导致另一个线程看到对象引用不是null,但是对象资源还没有完成初始化。
使用volatile关键字可以禁止指令重排,实现双重检查锁定。代码示例:
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;}
}
这里把instance变量声明为volatile,可以禁止指令重排,保证在对象完成初始化后,其他线程可以正确看到instance不为null。
这种方式是实现Singleton模式的最佳方式,它只有第一次调用getInstance方法时才会同步,这样既可以实现线程安全,又有很高的性能。
10. 案例:使用volatile实现中断机制
我们可以使用一个volatile变量作为中断标志,在循环体内检查这个变量,一次循环检查后立即重新读取变量的值,保证对变量修改的可见性,从而实现中断机制。
public class VolatileInterruptionDemo extends Thread { private volatile boolean interrupted = false;@Overridepublic void run() {while (!interrupted) {// do something}System.out.println("Interrupted!");}public void interrupt() {interrupted = true;}
}
这里的interrupted变量被声明为volatile,可以保证线程可以感知到中断信号,从循环体内退出。
这就是使用volatile实现的一种简单的中断机制,利用了volatile的可见性来保证线程可以正确读取到最新的中断标志。
11. 案例:使用AtomicInteger代替volatile
前面提到过,volatile不能保证原子性,要解决这个问题可以使用synchronized或Atomic类。这里我们通过一个例子来展示如何使用AtomicInteger代替volatile。
先看一个使用volatile的例子:
public class VolatileDemo {private volatile int counter = 0;public void increase() {counter++;}public int getCounter() {return counter;}
}
这里的counter++不是一个原子操作,在多线程环境下会存在数据竞争问题。
现在使用AtomicInteger代替:
public class AtomicDemo {private AtomicInteger counter = new AtomicInteger(0);public void increase() {counter.getAndIncrement();}public int getCounter() {return counter.get();}
}
AtomicInteger的getAndIncrement()方法是一个CAS原理的原子操作,可以保证线程安全。
AtomicInteger使用CAS操作实现原子操作,CAS操作包含三个操作:
- 获取变量的当前值V
- 对V的值进行操作
- 使用CAS操作设置变量的值,这个设置值的操作需要提供变量的当前值V和新值,当变量的当前值还是V时才会设置新值,否则重新获取当前值。
CAS操作可以保证如果在多个线程同时使用一个变量时,只有一个线程可以更新变量的值,其他线程的设置值操作都会失败,这种机制可以实现原子操作。
所以,通过这个例子我们可以看出,AtomicInteger是一个很好的替代volatile的选择,它可以保证原子性也具有volatile所有特性,性能也更好,是实现原子操作的最佳选择。
12. 案例:基于volatile实现一个简单的并发容器
这里我们实现一个简单的线程安全的容器,它只包含两个方法:add()和size()。
使用volatile和synchronized实现如下:
public class VolatileContainer {private volatile int size = 0;private Object[] items = new Object[10];public void add(Object item) {synchronized (items) {items[size] = item;size++;}}public int size() {return size;}
}
这里使用volatile声明size变量来保证线程安全,同时使用synchronized对items数组加锁来保证添加操作的原子性。
size()方法只需要简单的读取size变量,由于它被声明为volatile,可以保证每次得到的都是最新大小值。
这是一个使用volatile和synchronized实现的简单线程安全容器,利用了volatile的可见性和synchronized的互斥锁来保证线程安全。
相比直接对整个方法加锁,这种方式的性能会更好,因为size()方法没有加锁,可以并发执行,只有在必要的add()方法进行同步,这也体现了锁的精确性原则。
13. 小结
通过这几个案例,加深了对volatile和AtomicInteger的理解,主要体会到:
- volatile可以保证可见性和有序性,但不能保证原子性,要用synchronized或Atomic类补充。
- AtomicInteger可以完全替代volatile,并且性能更好,是原子操作的最佳选择。
- 合理使用volatile和锁可以实现较高性能的线程安全程序。锁的使用要遵循精确性原则,不要过度使用。
- volatile和AtomicInteger都是JMM的重要组成部分,理解它们的实现原理有助于使用它们。
14. 案例:使用AtomicStampedReference实现ABA问题的解决
ABA问题是这样的:如果一个变量V初次读取的值是A,它的值被改成了B,后来又被改回为A,那些个依赖于V没有发生改变的线程就会产生错误的依赖。
这个问题通常发生在使用CAS操作的并发环境中,我们可以使用版本号的方式来解决这个问题,每次变量更新的时候版本号加1,那么A->B->A这个过程就会被检测出来。
AtomicStampedReference就是用过这个原理来解决ABA问题的,它包含一个值和一个版本号,我们可以这样使用:
AtomicStampedReference<Integer> atomicRef = new AtomicStampedReference<>(100, 0);// 获取当前值和版本号
int stamp = atomicRef.getStamp();
int value = atomicRef.getReference();// 尝试设置新值和版本号
boolean success = atomicRef.compareAndSet(value, 101, stamp, stamp + 1);
if(success) {// 设置成功,获取新版本号stamp = atomicRef.getStamp();
}
这里当我们重新设置值100的时候,由于版本号已经变了,所以compareAndSet会失败,ABA问题就被解决了。
AtomicStampedReference是JUC包中用来解决ABA问题的重要工具类,实际项目中也广泛使用,它利用版本号的方式巧妙解决了这个并发编程中容易产生的问题。
另外,AtomicStampedReference的版本号使用的是int类型,所以在高并发场景下也可能存在循环的问题,这个时候可以使用时间戳方式生成版本号来避免,不过一般情况下AtomicStampedReference已经可以很好解决ABA问题。
15. 总结
OK,到这里volatile相关内容就全部介绍完了,包括:
- volatile的定义及作用:可见性、有序性和禁止优化。
- volatile的底层实现原理:JMM、缓存一致性协议和内存屏障。
- volatile的使用实例:双重检查锁定和中断机制等。
- 如何解决volatile的原子性问题:使用synchronized和Atomic类。
- AtomicStampedReference用法和ABA问题解决。
- 一些volatile的最佳实践。
- 使用volatile和锁实现的一个简单线程安全容器。
讲解的内容比较广泛,试着结合理论和实践的方式进行解释,希望可以对大家理解volatile和并发编程有所帮助。这也是我学久而久之总结的一些心得体会,与大家共同分享学习。如果 对volatile和JMM还有哪些不理解的地方,也欢迎留言讨论,我们共同进步!再次感谢阅读这篇博客,也希望您能够在学习和工作中很好地应用volatile关键字!
相关文章:
volatile关键字原理的使用介绍和底层原理解析和使用实例
文章目录 volatile关键字原理的使用介绍和底层原理解析和使用实例1. volatile 关键字的作用2. volatile 的底层原理3. volatile 的使用案例4. volatile 的原子性问题5. 如何解决 volatile 的原子性问题6. volatile 的实现原理7. 小结8. volatile的最佳实践9. 案例:使用volatile…...

【软件下载】换新电脑记录下下载的软件时所需地址
1.idea https://www.jetbrains.com/zh-cn/idea/download/other.html 2.oracle官方(下载jdk时找的) https://www.oracle.com/ 3.jdk8 https://www.oracle.com/java/technologies/downloads/ 下拉找到jdk8 切换windows (需要注册个oracle账…...

【10.HTML入门知识-CSS元素定位】
1 标准流(Normal Flow) 默认情况下,元素都是按照normal flow(标准流、常规流、正常流、文档流【document flow】)进行排布 从左到右、从上到下按顺序摆放好 默认情况下,互相之间不存在层叠现象 1.1…...
LeetCode_贪心算法_简单_455.分发饼干
目录 1.题目2.思路3.代码实现(Java) 1.题目 假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。 对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的…...

HashMap
目录 HashMap是什么? 为什么要使用HashMap? HashMap存储元素原理(put⽅法) 扰动函数 前置知识 异或运算 &运算 为什么使用扰动函数 实验验证扰动函数 常见问题 HashMap的默认长度是多少? HashMap是先扩…...

数据结构初阶 —— 树(堆)
目录 一,堆 堆的概念 向下调整法(数组) 向上调整法(数组) 堆的创建(建堆) 堆的实现 一,堆 堆的概念 如有个关键码的集合K{,,,...…...

一文看懂低代码,5分钟从入门到原理全搞定
全球低代码市场已经走过了近20年,中国低代码市场近5年经历了百花齐放的广泛探索阶段,更旺盛的市场需求逐步在被激发。现在,让我们按下暂停键,看看这些产品给我们呈现了低代码市场一幅怎样的百景图。 低代码平台简介 广义上的低代…...

MetaERP系统主要干什么的,华为自研ERP的路子是否可以效仿?
近日,华为成功研发出自主可控的MetaERP系统,并完成了对旧有ERP系统的替换。该系统采用全栈自主可控技术,基于华为欧拉操作系统、GaussDB等根技术,采用云原生架构、元数据多租架构、实时智能技术等,提高业务效率&#x…...

自动驾驶——离散LQR的黎卡提方程Riccati公式推导与LQR工程化
1.LQR Question Background 之前写过连续系统的黎卡提方程Riccati推导,但是考虑到实际工程落地使用的是离散系统,于是又进行了离散黎卡提方程Riccati的公式推导。 2.Proof of Riccati Equation Formula for Discrete Systems 工程化落地,就…...

28.Mybatis的入门
目录 一、Mybatis的入门。 (1)Mybatis的简介。 (2)Mybatis的快速入门。 (2.1)快速入门。 (2.2)UserMapper.xml文件。 (2.3)sqlMapConfig.xml文件。 …...
Android Jetpack 从使用到源码深耕【ViewModel从实践到原理 】(三)
上文,我们通过简单的ViewModel使用源码入手,对其源码进行阅读,原理进行了简单总结,简单来说,ViewModel是通过Activity的onRetainNonConfigurationInstance 与 getLastNonConfigurationInstance的自动调用,实现了 ViewModel数据的存储和恢复,数据存储在ViewModelStore的m…...
什么性格的人适合报考环境科学类专业?高考选专业
环境科学类专业包括有:环境科学与工程,环境工程,环境科学,环境生态工程,环保设备工程,资源环境科学,水质科学与技术。 环境对于未来是一个极其重要的方向,需要学生具备一定的科学素…...
Python中的异常处理机制可以帮助程序员在程序运行过程中遇到错误时进行处理
Python中的异常处理机制可以帮助程序员在程序运行过程中遇到错误时进行处理,防止程序崩溃或出现不可预测的错误。 Python中的异常处理使用try-except语句。try语句块包含可能会出现异常的代码,而except语句块则定义了出现异常时应该执行的操作。下面是一…...

TCP之报文格式解析
TCP网络协议是较常用的,也基本上都会接触,那么来简单了解下它吧。TCP 是一种面向连接的、可靠的传输协议,它能够将数据分成一些小块,并通过 Internet 进行传输。在 TCP 中,数据被分割成一些称为 TCP 报文段(…...
qemu-基础篇(二)——裸机 arm 程序环境搭建
文章目录 测试代码makefile运行 qemu调试 qemuGDB 常用命令 裸机篇系列文章主要用于熟悉 arm 汇编及处理器结构 测试代码 _start:ldr r0, 0X020C4068 /* CCGR0 */ldr r1, 0XFFFFFFFF str r1, [r0]ldr r0, 0X020C406C /* CCGR1 */str r1, [r0]ldr r0, 0X020C4070 …...
JSP+SQL基于JSP的学生信息管理系统(源代码+论文+答辩PPT)
随着学校规模的不断扩大,学生数量急剧增加,有关学生的各种信息也成倍增长。面对如此庞大的信息量,开发学生信息管理系统来提高学生管理工作的效率就成为必然。通过该系统,可以做到信息的规范管理、科学统计和快速查询,…...

docker上部署程序后无法连接数据库的问题
咱就是说,这个问题差点给我劝退docker。下面说下环境情况。 装了个javaweb程序容器,装了个数据库容器,javaweb容器就是链接不上数据库。 咱也是跟着菜鸟教程的容器互联步骤简历网络链接: 并且启动时增加--networkxxx 都加入到了…...

Ucore lab4
实验目的 了解内核线程创建/执行的管理过程了解内核线程的切换和基本调度过程 实验内容 练习一:分配并初始化一个进程控制块 1.内核线程及管理 内核线程是一种特殊的进程,内核线程与用户进程的区别有两个:内核线程只运行在内核态&#x…...

AI失业潮来袭,某些部门裁员过半
历史的车轮滚滚向前,每次生产力的大幅跃进,都会造成一批失业潮。想当年,纺纱机的出现让无数手工作坊的织布师傅失业。如今,在AI技术的催化下,同样的事正在互联网行业的各个领域重演。 疯狂的裁员浪潮 “AI15秒做的&am…...

git 撤销add/commit,以及更换源命令
前言:主要是为了自己方便记录,省的每次都查找一下这些命令 1、当我们只是想撤回commit,保留add .的时候,可以用下方代码 git reset --soft HEAD^ 2、当我们想撤回commit以及add .的时候,可以用下方代码 git reset…...

19c补丁后oracle属主变化,导致不能识别磁盘组
补丁后服务器重启,数据库再次无法启动 ORA01017: invalid username/password; logon denied Oracle 19c 在打上 19.23 或以上补丁版本后,存在与用户组权限相关的问题。具体表现为,Oracle 实例的运行用户(oracle)和集…...
synchronized 学习
学习源: https://www.bilibili.com/video/BV1aJ411V763?spm_id_from333.788.videopod.episodes&vd_source32e1c41a9370911ab06d12fbc36c4ebc 1.应用场景 不超卖,也要考虑性能问题(场景) 2.常见面试问题: sync出…...

Lombok 的 @Data 注解失效,未生成 getter/setter 方法引发的HTTP 406 错误
HTTP 状态码 406 (Not Acceptable) 和 500 (Internal Server Error) 是两类完全不同的错误,它们的含义、原因和解决方法都有显著区别。以下是详细对比: 1. HTTP 406 (Not Acceptable) 含义: 客户端请求的内容类型与服务器支持的内容类型不匹…...

iPhone密码忘记了办?iPhoneUnlocker,iPhone解锁工具Aiseesoft iPhone Unlocker 高级注册版分享
平时用 iPhone 的时候,难免会碰到解锁的麻烦事。比如密码忘了、人脸识别 / 指纹识别突然不灵,或者买了二手 iPhone 却被原来的 iCloud 账号锁住,这时候就需要靠谱的解锁工具来帮忙了。Aiseesoft iPhone Unlocker 就是专门解决这些问题的软件&…...

渗透实战PortSwigger靶场-XSS Lab 14:大多数标签和属性被阻止
<script>标签被拦截 我们需要把全部可用的 tag 和 event 进行暴力破解 XSS cheat sheet: https://portswigger.net/web-security/cross-site-scripting/cheat-sheet 通过爆破发现body可以用 再把全部 events 放进去爆破 这些 event 全部可用 <body onres…...
Go 语言接口详解
Go 语言接口详解 核心概念 接口定义 在 Go 语言中,接口是一种抽象类型,它定义了一组方法的集合: // 定义接口 type Shape interface {Area() float64Perimeter() float64 } 接口实现 Go 接口的实现是隐式的: // 矩形结构体…...

深入理解JavaScript设计模式之单例模式
目录 什么是单例模式为什么需要单例模式常见应用场景包括 单例模式实现透明单例模式实现不透明单例模式用代理实现单例模式javaScript中的单例模式使用命名空间使用闭包封装私有变量 惰性单例通用的惰性单例 结语 什么是单例模式 单例模式(Singleton Pattern&#…...

基于当前项目通过npm包形式暴露公共组件
1.package.sjon文件配置 其中xh-flowable就是暴露出去的npm包名 2.创建tpyes文件夹,并新增内容 3.创建package文件夹...

srs linux
下载编译运行 git clone https:///ossrs/srs.git ./configure --h265on make 编译完成后即可启动SRS # 启动 ./objs/srs -c conf/srs.conf # 查看日志 tail -n 30 -f ./objs/srs.log 开放端口 默认RTMP接收推流端口是1935,SRS管理页面端口是8080,可…...
Nginx server_name 配置说明
Nginx 是一个高性能的反向代理和负载均衡服务器,其核心配置之一是 server 块中的 server_name 指令。server_name 决定了 Nginx 如何根据客户端请求的 Host 头匹配对应的虚拟主机(Virtual Host)。 1. 简介 Nginx 使用 server_name 指令来确定…...