Java进阶笔记——你需要了解的volatile 关键字
前言
不管是在面试还是实际开发中 volatile
都是一个应该掌握的技能。
首先来看看为什么会出现这个关键字。
内存可见性
由于 Java
内存模型(JMM
)规定,所有的变量都存放在主内存中,而每个线程都有着自己的工作内存(高速缓存)。
线程在工作时,需要将主内存中的数据拷贝到工作内存中。这样对数据的任何操作都是基于工作内存(效率提高),并且不能直接操作主内存以及其他线程工作内存中的数据,之后再将更新之后的数据刷新到主内存中。
这里所提到的主内存可以简单认为是堆内存,而工作内存则可以认为是栈内存。
如下图所示:
所以在并发运行时可能会出现线程 B 所读取到的数据是线程 A 更新之前的数据。
显然这肯定是会出问题的,因此 volatile
的作用出现了:
当一个变量被
volatile
修饰时,任何线程对它的写操作都会立即刷新到主内存中,并且会强制让缓存了该变量的线程中的数据清空,必须从主内存重新读取最新数据。
volatile
修饰之后并不是让线程直接从主内存中获取数据,依然需要将变量拷贝到工作内存中。
内存可见性的应用
当我们需要在两个线程间依据主内存通信时,通信的那个变量就必须的用 volatile
来修饰:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | public class Volatile implements Runnable{ private static volatile boolean flag = true ; @Override public void run() { while (flag){ } System.out.println(Thread.currentThread().getName() +"执行完毕"); } public static void main(String[] args) throws InterruptedException { Volatile aVolatile = new Volatile(); new Thread(aVolatile,"thread A").start(); System.out.println("main 线程正在运行") ; Scanner sc = new Scanner(System.in); while(sc.hasNext()){ String value = sc.next(); if(value.equals("1")){ new Thread(new Runnable() { @Override public void run() { aVolatile.stopThread(); } }).start(); break ; } } System.out.println("主线程退出了!"); } private void stopThread(){ flag = false ; } } |
主线程在修改了标志位使得线程 A 立即停止,如果没有用 volatile
修饰,就有可能出现延迟。
但这里有个误区,这样的使用方式容易给人的感觉是:
对
volatile
修饰的变量进行并发操作是线程安全的。
这里要重点强调,volatile
并不能保证线程安全性!
如下程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | public class VolatileInc implements Runnable{ private static volatile int count = 0 ; //使用 volatile 修饰基本数据内存不能保证原子性 //private static AtomicInteger count = new AtomicInteger() ; @Override public void run() { for (int i=0;i<10000 ;i++){ count ++ ; //count.incrementAndGet() ; } } public static void main(String[] args) throws InterruptedException { VolatileInc volatileInc = new VolatileInc() ; Thread t1 = new Thread(volatileInc,"t1") ; Thread t2 = new Thread(volatileInc,"t2") ; t1.start(); //t1.join(); t2.start(); //t2.join(); for (int i=0;i<10000 ;i++){ count ++ ; //count.incrementAndGet(); } System.out.println("最终Count="+count); } } |
当我们三个线程(t1,t2,main)同时对一个 int
进行累加时会发现最终的值都会小于 30000。
这是因为虽然
volatile
保证了内存可见性,每个线程拿到的值都是最新值,但count ++
这个操作并不是原子的,这里面涉及到获取值、自增、赋值的操作并不能同时完成。
-
所以想到达到线程安全可以使这三个线程串行执行(其实就是单线程,没有发挥多线程的优势)。
-
也可以使用
synchronize
或者是锁的方式来保证原子性。 -
还可以用
Atomic
包中AtomicInteger
来替换int
,它利用了CAS
算法来保证了原子性。
指令重排
内存可见性只是 volatile
的其中一个语义,它还可以防止 JVM
进行指令重排优化。
举一个伪代码:
1 2 3 | int a=10 ;//1 int b=20 ;//2 int c= a+b ;//3 |
一段特别简单的代码,理想情况下它的执行顺序是:1>2>3
。但有可能经过 JVM 优化之后的执行顺序变为了 2>1>3
。
可以发现不管 JVM 怎么优化,前提都是保证单线程中最终结果不变的情况下进行的。
可能这里还看不出有什么问题,那看下一段伪代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | private static Map<String,String> value ; private static volatile boolean flag = fasle ; //以下方法发生在线程 A 中 初始化 Map public void initMap(){ //耗时操作 value = getMapValue() ;//1 flag = true ;//2 } //发生在线程 B中 等到 Map 初始化成功进行其他操作 public void doSomeThing(){ while(!flag){ sleep() ; } //dosomething doSomeThing(value); } |
这里就能看出问题了,当 flag
没有被 volatile
修饰时,JVM
对 1 和 2 进行重排,导致 value
都还没有被初始化就有可能被线程 B 使用了。
所以加上 volatile
之后可以防止这样的重排优化,保证业务的正确性。
指令重排的的应用
一个经典的使用场景就是双重懒加载的单例模式了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public class Singleton { private static volatile Singleton singleton; private Singleton() { } public static Singleton getInstance() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { //防止指令重排 singleton = new Singleton(); } } } return singleton; } } |
这里的 volatile
关键字主要是为了防止指令重排。
如果不用 ,singleton = new Singleton();
,这段代码其实是分为三步:
- 分配内存空间。(1)
- 初始化对象。(2)
- 将
singleton
对象指向分配的内存地址。(3)
加上 volatile
是为了让以上的三步操作顺序执行,反之有可能第二步在第三步之前被执行就有可能某个线程拿到的单例对象是还没有初始化的,以致于报错。
总结
volatile
在 Java
并发中用的很多,比如像 Atomic
包中的 value
、以及 AbstractQueuedLongSynchronizer
中的 state
都是被定义为 volatile
来用于保证内存可见性。
将这块理解透彻对我们编写并发程序时可以提供很大帮助。
欢迎工作一到五年的Java工程师朋友们加入Java架构开发:468947140
点击链接加入群聊【Java-BATJ企业级资深架构】:https://jq.qq.com/?_wv=1027&k=52j2FVO
本群提供免费的学习指导 架构资料 以及免费的解答
不懂得问题都可以在本群提出来 之后还会有职业生涯规划以及面试指导
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
[转]使用 Homestead 作为 tp5 的开发环境配置
这是迄今为止最优雅的解决方案了,亲测有效 在 Homestead/scripts 目录下新建文件 serve-tp5.sh #!/usr/bin/env bash declare -A params=$6 # Create an associative array paramsTXT="" if [ -n "$6" ]; then for element in "${!params[@]}" do paramsTXT="${paramsTXT} fastcgi_param ${element} ${params[$element]};" done fi block="server { listen ${3:-80}; listen ${4:-443} ssl http2; server_name .$1; root \"$2\"; index index.html index.htm index.php; charset utf-8; location / { #try_files \$uri \$uri/ /index.php?\$query_string; if (!-e \$re...
- 下一篇
正则表达式(java)
语法 正则.png 正则.png 正则.png 典型例子 ^\d+$//匹配非负整数(正整数 + 0) ^[0-9][1-9][0-9]$//匹配正整数 ^((-\d+)|(0+))$//匹配非正整数(负整数 + 0) ^-[0-9][1-9][0-9]$//匹配负整数 ^-?\d+$//匹配整数 ^\d+(.\d+)?$//匹配非负浮点数(正浮点数 + 0) ^(([0-9]+.[0-9][1-9][0-9])|([0-9][1-9][0-9].[0-9]+)|([0-9][1-9][0-9]))$//匹配正浮点数 ^((-\d+(.\d+)?)|(0+(.0+)?))$//匹配非正浮点数(负浮点数 + 0) ^(-(([0-9]+.[0-9][1-9][0-9])|([0-9][1-9][0-9].[0-9]+)|([0-9][1-9][0-9])))$//匹配负浮点数 ^(-?\d+)(.\d+)?$//匹配浮点数 ^[A-Za-z]+$//匹配由26个英文字母组成的字符串 ^[A-Z]+$//匹配由26个英文字母的大写组成的字符串 ^[a-z]+$//匹配由26个英文字母的小写...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- MySQL8.0.19开启GTID主从同步CentOS8
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- CentOS关闭SELinux安全模块
- CentOS7设置SWAP分区,小内存服务器的救世主
- Docker安装Oracle12C,快速搭建Oracle学习环境
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作