李林超博客
首页
归档
留言
友链
动态
关于
归档
留言
友链
动态
关于
首页
Java
正文
【转载】volatile关键字简介
Leefs
2020-02-18 AM
1293℃
0条
# 【转载】volatile关键字简介 **Java线程控制中常见的两个关键字:synchronized、volatile** ### 一、volatile是什么? volatile是Java中的关键字,也是Java虚拟机提供的轻量级的同步机制(乞丐版的synchronized)。 ### 二、volatile的三大特性 > 1. 1.可见性 > 2. 2.不保证原子性 > 3. 3.禁止指令重排序 ### 三、为什么说volatile是轻量级的同步机制? 因为大多数多线程开发都需要遵守JMM的三大特性: > 1. 1.可见性 > 2. 2.原子性 > 3. 3.有序性 而volatile只保证可见性和禁止指令重排序(有序性)所以说是轻量级的同步机制。 #### 可见性 由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程私有数据区域,而Java内存模型中规定所有变量都存在主内存,主内存是共享区域,所有线程都可以访问,但线程对变量的操作(读取、赋值等)必须在工作内存中进行,首先,要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存的变量副本拷贝,因此不同线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存完成。 **例:** ![36.volatile关键字01.png][1] 现在这里有三个线程:线程1,线程2,线程3想要去改主内存的age,它不是直接去主内存修改,而是先把主内存的age拷贝到自己的工作内存,然后在自己的工作内存进行修改,再写到主内存,比如这里线程1把25改成37再写回去,但是线程2和线程3并不知道主内存的值从25改成37,所以我们必须要有一种机制,只要有一个线程修改了自己工作内存的值并写回给主内存以后,要及时通知其他线程 **这样及时通知这种情况是JMM内存模型的第一个重要特性:可见性。** 代码示例: 没有用volatile修饰 ```java class Demo { int num = 0; public void add() { this.num = 60; } } /** * 验证valatile的可见性 */ public class Test { public static void main(String[] args) { Demo d=new Demo();// 线程操作资源类 new Thread(() -> { System.out.println(Thread.currentThread().getName()+"\t come in:"+d.num); try { TimeUnit.SECONDS.sleep(3); }catch (Exception e) { e.printStackTrace(); } d.add(); System.out.println(Thread.currentThread().getName()+"\t update num value:"+d.num); },"线程1").start(); while(d.num == 0) { // main线程一直等待循环,直到num值不再等于0. } System.out.println(Thread.currentThread().getName()+"\t over:"+d.num); } } ``` **运行结果:** ``` 线程1 come in:0 线程1 update num value:60 ``` 我们可以看到线程1把num的值已经改成60了,也就是说线程1已经把60写回主内存了,但是程序没有停止,那就是说main线程不知道值已经改为60了,但没有人通知main线程 **加上volatile:** ```java class Demo { volatile int num = 0; public void add() { this.num = 60; } } /** * 验证valatile的可见性 */ public class Test { public static void main(String[] args) { Demo d=new Demo();// 线程操作资源类 new Thread(() -> { System.out.println(Thread.currentThread().getName()+"\t come in:"+d.num); try { TimeUnit.SECONDS.sleep(3); }catch (Exception e) { e.printStackTrace(); } d.add(); System.out.println(Thread.currentThread().getName()+"\t update num value:"+d.num); },"线程1").start(); while(d.num == 0) { // main线程一直等待循环,直到num值不再等于0. } System.out.println(Thread.currentThread().getName()+"\t over:"+d.num); } } ``` **运行结果:** ``` 线程1 come in:0 线程1 update num value:60 main over:60 ``` 只要有一个线程修改了主内存的值,马上写回去的时候,只要加了volatile关键字的变量,其他线程迅速受到通知,拿到主内存最新的值。 #### 不保证原子性 原子性指的是什么? 不可分割,完整性,也就是说某个线程正在做某个具体业务时,中间不可以被加塞或被分割,需要整体完整,要么同时成功,要么同时失败。 **代码验证:** ```java class Demo { volatile int num = 0; // 此时num前面是加了volatile关键字修饰的,volatile不保证原子性 public void add() { num++; } } /** * 验证valatile不保证原子性 */ public class Test { public static void main(String[] args) { Demo d=new Demo();// 线程操作资源类 for(int i = 0;i <20; i++){ new Thread(() -> { for (int j = 0; j <1000 ; j++) { d.add(); } },String.valueOf(i)).start(); } while(Thread.activeCount() > 2) { Thread.yield(); } System.out.println(Thread.currentThread().getName()+"\t num value:"+d.num); } } ``` **运行结果** ``` main num value:19345 ``` 这里有20个线程,每个线程执行1000次,按理来说最终运行结果应该是20000才对,但是每次的运行结果都不一样。 #### volatile不保证原子性的解释 ![36.volatile关键字02.png][2] 假设这里有三个线程操作主内存数据,但不能直接操作主内存,要把它拷贝回自己的工作空间,所谓的变量的副本拷贝,这里每个线程工作内存都为0,这时候都在自己工作内存进行累加:线程1工作内存值为1,线程2工作内存也为1,线程3工作内存也是1,只要线程的工作内存操作完成后会写回主内存。 假设说线程1和线程2同时读到了这时候它们的工作内存值都为0,正常情况下线程1先写回去,主内存值为1了,线程2再拿到这个值进行累加再写回来,这个时候主内存值应该为2了,但是由于多线程竞争和调度的关系,某一个时间段线程1和线程2的值都为0,各自在自己的工作内存进行累加,准备把这个1写回去,将会出现在某一时间段,线程1将要把1写回去的时候突然被挂起了,线程2再刷的一下把它工作内存的1写回去,然后线程2再通知其他线程,其他线程还没有反应过来的情况下,被挂起的线程1马上也写回去,本来线程1和线程2应该加两次,但是会出现线程1的值1把线程2写回去的值1覆盖了,出现了丢失数据的一次。这就是数值为什么达不到20000的原因,出现丢失写值的情况。 #### 禁止指令重排 计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排,一般分以下3中: > 源代码——》编译器优化的重排——》指令并行的重排——》内存系统的重排——》最终执行的指令 在单线程环境里确保程序最终执行结果和代码顺序执行的结果一致。 处理器在进行重排序时要考虑指令之间的数据依赖性。 多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。 **代码** ```java int a = 0; boolean flag = false; public void method01() { a = 1; flag = true; } // 多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。 public void method02() { if(flag) { a = a + 5; System.out.println("return value:"+a); } } ``` #### volatile禁止指令重排总结 volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象。 先了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个: 1.保证特定操作的执行顺序; 2.保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。 由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。 ![36.volatile关键字03.png][3] ![36.volatile关键字04.png][4] *附:[原文链接](https://www.cnblogs.com/chengcanhua/p/11820171.html)* [1]: https://lilinchao.com/usr/uploads/2020/02/4001473709.png [2]: https://lilinchao.com/usr/uploads/2020/02/2099558782.png [3]: https://lilinchao.com/usr/uploads/2020/02/2090254447.png [4]: https://lilinchao.com/usr/uploads/2020/02/761871215.png
标签: none
非特殊说明,本博所有文章均为博主原创。
如若转载,请注明出处:
https://www.lilinchao.com/archives/630.html
上一篇
LeetCode-3 无重复字符的最长子串
下一篇
LeetCode-4 寻找两个有序数组的中位数
取消回复
评论啦~
提交评论
栏目分类
随笔
2
Java
326
大数据
229
工具
31
其它
25
GO
47
标签云
Golang
字符串
Elasticsearch
Golang基础
散列
Java
ClickHouse
栈
JavaSE
设计模式
二叉树
正则表达式
Zookeeper
数据结构和算法
Yarn
Flink
Spark Streaming
Ubuntu
Stream流
数学
稀疏数组
Livy
Http
随笔
FastDFS
ajax
哈希表
持有对象
Java编程思想
MyBatisX
友情链接
申请
范明明
庄严博客
Mx
陶小桃Blog
虫洞