查看“Volatile”的源代码
←
Volatile
跳到导航
跳到搜索
因为以下原因,您没有权限编辑本页:
您所请求的操作仅限于该用户组的用户使用:
用户
您可以查看和复制此页面的源代码。
volatile 的使用是为了线程安全,但 volatile 不保证线程安全。 线程安全有三个要素:可见性、有序性和原子性。 线程安全是指在多线程情况下,对共享内存的使用,不会因为不同线程的访问和修改发生与预期不符的情况。 === volatile 的作用 === volatile 有以下三个作用: ==== volatile 用于解决多核 CPU cache(高速缓存)导致的变量不同步 ==== 这本质上是个硬件问题,其根源在于:CPU 的高速缓存的读取速度远远快于主存(物理内存)。 所以,CPU 在读取一个变量的时候,会把数据先读取到缓存,这样下次再访问同一个数据的时候就可以直接从缓存读取了,显然提高了读取的性能。 而多核 CPU 有多个这样的缓存。这就带来了问题,当某个 CPU(例如 CPU1)修改了这个变量(比如把 a 的值从 1 修改为 2 ),但是其他的 CPU(例如 CPU2)在修改前已经把 a=1 读取到自己的缓存了,当 CPU2 再次读取数据的时候,它仍然会去自己的缓存区中读取,此时读取到的值仍然是 1,但是实际上这个值已经变成 2 了。 这里,就涉及了线程安全的要素:'''<big>可见性</big>'''。 可见性是指当多个线程在访问同一个变量时,如果其中一个线程修改了变量的值,那么其他线程应该能立即看到修改后的值。 volatile 的实现原理是内存屏障(Memory Barrier),其原理为:当 CPU 写数据时,如果发现一个变量在其他 CPU 中存有副本,那么会发出信号通知其他 CPU 将该副本对应的缓存行置为无效状态,当其他 CPU 读取到 变量副本的时候,会发现该缓存行是无效的,然后,它会从主存重新读取变量。 ==== volatile 可以解决指令重排序的问题 ==== 一般情况下,程序是按照顺序执行的,例如下面的代码:<syntaxhighlight lang="java" line="1"> int i = 0; i++; boolean f = false; f = true; </syntaxhighlight>如果 <code>i++</code> 发生在 <code>int i = 0</code> 之前,那么会不可避免地出错,CPU 在执行代码对应指令的时候,会认为 1、2 两行是具备依赖性的,因此,CPU 一定会安排行 1 早于行 2 执行。 那么,<code>int i = 0</code> 一定会早于 <code>boolean f = false</code> 吗? 并不一定,CPU 在运行期间会对指令进行优化,没有依赖关系的指令,它们的顺序可能会被重排。 在单线程执行的情况下,发生重排是没有问题的,CPU 保证了顺序不一定一致,但结果一定一致(重排前和重排后指令执行得到的结果一致)。 但在多线程环境下,重排序则会引起很大的问题,这又涉及了线程安全的要素:'''<big>有序性</big>''' 有序性是指程序执行的顺序应当按照代码的先后顺序执行。 为了更好地理解有序性,下面通过一个例子来分析:<syntaxhighlight lang="java"> //成员变量 i int i = 0; //线程一的执行代码 Thread.sleep(10); i++; f = true; //线程二的执行代码 while(!f){ System.out.println(i); } </syntaxhighlight>理想的结果应该是:线程二不停地打印 0,最后打印一个1,终止。 在线程一里,f 和 i 没有依赖性,如果发生了指令重排,那么 f = true 发生在 i++ 之前,就有可能导致线程二在终止循环前输出的全部是 0。 需要注意的是,这种情况并不常见,再次运行并不一定能重现,正因为如此,很可能会导致出现一些莫名的问题。 如果修改上方代码中 i 的定义为使用 volatile 关键字来修饰,那么就可以保证最后的输出结果符合预期。 这是因为,被 volatile 修饰的变量,CPU 不会对它做重排序优化,所以也就保证了有序性。 ==== volatile 不保证操作的原子性 ==== 原子性:一个或多个操作,要么全部连续执行且不会被任何因素中断,要么就都不执行。 一眼看上去,这个概念和数据库概念里的事务(Transaction)很类似,没错,事务就是一种原子性操作。 原子性、可见性和有序性,是线程安全的三要素。 需要特别注意的是,volatile 保证可见性和有序性,但是不保证操作的原子性,下面的代码将会证明这一点:<syntaxhighlight lang="java"> import java.util.HashMap; import java.util.Map; public class TestVolatile3 { static volatile int intVal = 0; public static void main(String[] args) { Map<Long, Long> map = new HashMap<>(); //创建10个线程,执行简单的自加操作 for (int i = 0; i < 10; i++) { new Thread(() -> { for (int j = 0; j < 1000; j++) { intVal++; if (j == 999) { System.out.printf("id:%d 已经循环到999\n", Thread.currentThread().getId()); } } }).start(); } // 保证之前启动的全部线程执行完毕 while(Thread.activeCount() > 1) { Thread.yield(); } System.out.println(intVal); } } </syntaxhighlight>注意:上面代码在 Windows 命令行中运行没有问题:<syntaxhighlight lang="powershell"> E:\record\2023\3\2>javac TestVolatile3.java -encoding utf-8 E:\record\2023\3\2>java TestVolatile3 id:13 已经循环到 999 id:17 已经循环到 999 id:16 已经循环到 999 id:19 已经循环到 999 id:21 已经循环到 999 id:20 已经循环到 999 id:22 已经循环到 999 id:14 已经循环到 999 id:18 已经循环到 999 id:15 已经循环到 999 7759 </syntaxhighlight>可以进行测试 '''但在 IDEA 中运行的话会出问题:IDEA 启动程序后出了有“main”线程,还会多出一个“Monitor Ctrl-Break”的线程,所以会因 <code>while(Thread.activeCount() > 1)</code> 陷入死循环''' 在之前的内容有提及,volatile 能保证修改后的数据对所有线程可见,那么,这一段对 intVal 自增的代码,最终执行完毕的时候,intVal 应该为 10000。 但事实上,结果是不确定的,大部分情况下会小于 10000。这是因为,无论是 volatile 还是自增操作,都不具备原子性。 假设 intVal 初始值为 100,自增操作的指令执行顺序如下所示: # 获取 intVal,此时主存内 intVal 值为 100; # intVal 执行 +1, 得到 101,此时主存内 intVal 值仍然为 100; # 将 101 写回给 intVal,此时主存内 intVal 值从 100 变化为 101。 具体执行流程如图: [[文件:自增操作实现原理.png|无|缩略图|362x362像素]]这个过程很容易理解,如果这段指令发生在多线程环境下呢? 以下面这段会发生错误的指令顺序为例: # 线程一获得了 intVal 值为 100; # 线程一执行+1,得到 101,此时值没有写回给主存; # 线程二在主存内获得了 intVal 值为 100; # 线程二执行+1,得到 101; # 线程一写回 101; # 线程二写回 101; 于是,最终主存内的 intVal 值,还是 101。具体执行流程如图: [[文件:多线程执行自增操作的结果.png|无|缩略图|512x512像素]] 为什么 volatile 的可见性保证在这里没有生效? 根据 volatile 保证可见性的原理(内存屏障),当一个线程执行写的时候,才会改变“数据修改”的标量,在上述过程中,线程一在执行加法操作发生后,写回操作发生前,CPU 开始处理线程二的时间片,执行了另外一次读取 intVal,此时 intVal 值为 100,且由于写回操作尚未发生,这一次读取是成功的。 因此,出现了最后计算结果不符合预期的情况。 synchronized 关键字确实可以解决多线程的原子操作问题,可以修改上面的代码为:<syntaxhighlight lang="java"> Object lock = new Object(); for (int i = 0; i < 10; i++) { new Thread(() -> { synchronized (lock) { for (int j = 0; j < 1000; j++) { intVal++; } } }).start(); } </syntaxhighlight>但是,这种方式明显效率不高,10个线程都在争抢同一个代码块的使用权。(其实可以通过 CAS 来保证原子性可以获得更高的效率) 由此可见,volatile 只能提供线程安全的两个必要条件:可见性和有序性。
返回至
Volatile
。
导航菜单
个人工具
登录
名字空间
页面
讨论
变种
视图
阅读
查看源代码
查看历史
更多
搜索
导航
首页
Spring Boot 2 零基础入门
Spring Cloud
Spring Boot
设计模式之禅
VUE
Vuex
Maven
算法
技能树
Wireshark
IntelliJ IDEA
ElasticSearch
VirtualBox
软考
正则表达式
程序员精讲
软件设计师精讲
初级程序员 历年真题
C
SQL
Java
FFmpeg
Redis
Kafka
MySQL
Spring
Docker
JMeter
Apache
Linux
Windows
Git
ZooKeeper
设计模式
Python
MyBatis
软件
数学
PHP
IntelliJ IDEA
CS基础知识
网络
项目
未分类
MediaWiki
镜像
问题
健身
国债
英语
烹饪
常见术语
MediaWiki帮助
工具
链入页面
相关更改
特殊页面
页面信息