面试(三)---volatile
一、前言
最近去成都玩了一圈,整体感觉还不错,辞职以后工作找的也很顺利,随着年龄增加自己也考虑定居和个人长期发展的问题,反正乱七八糟的事,总之需要好好屡屡思路,不能那么着急下定论,当然我对下份工作也是有所期望的,不扯了开始我们今天主题吧。
二、Java的内存模型
Java内存模型规定所有的变量都存在主内存当中,每条线程都有自己的工作内存,线程的工作内存保存了被该线程使用的变量的主内存副本拷贝,线程对变量的所有操作都在内存中进行,而不能直接读写主内存中的变量。不同的线程之间无法直接访问对工作内存中的变量,线程之间变量值的传递均需要通过主内存来完成。----来自深入理解Java虚拟机

这里注意下,Java的内存模型(JMM)和Java的内存区域的差别,不要混淆这两者之间的概念,JMM主要是围绕在程序中各个变量在共享数据区域和私有数据区域的访问方式。JMM与Java内存区域唯一相似点,都存在共享数据区域和私有数据区域,在JMM中主内存属于共享数据区域,从某个程度上讲应该包括了堆和方法区,而工作内存数据线程私有数据区域,从某个程度上讲则应该包括程序计数器、虚拟机栈以及本地方法栈。或许在某些地方,我们可能会看见主内存被描述为堆内存,工作内存被称为线程栈,实际上他们表达的都是同一个含义。
接下来我们来看下主内存和工作内存之间的交互问题:

1.lock操作,锁定主内存变量,标识为当前线程独占状态;
2.read操作,将锁定的主内存变量读取到工作内存当中;
3.load操作,将read操作的主线程变量放入到工作变量的副本当中;
4.use操作,将工作内存的变量传递给执行引擎,当虚拟机调用到该变量的时候执行该变量;
5.assign操作,当虚拟机对工作内存的变量的值进行赋值操作的时候,将值赋值给工作内存的变量;
6.store操作,将工作内存的变量传递给主内存变量;
7.write操作,将store操作的工作变量写入工作变量;
8.unlock操作,将锁定的主内存中的变量锁释放,等待其他线程锁定;
明白这些我们再来探究下JMM如何处理原子性、可见性和有序性的问题:
1.原子性
Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。
2.可见性
由于JMM结构原因,当多线程访问的时候不能及时将变量的值更新到主内存当中,这个时候就能出现数据不一致的问题,Java提供了volatile关键字来保证可见性,另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。
3.有序性
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
JMM中有序性可以通过volatile实现,另外通过synchronized和Lock也能够保证有序性,这个和保证可见性的原理一样;另外Java语言有一个“先行发生(happens-before)”的原则,如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。
下面就来具体介绍下happens-before原则(先行发生原则):
1.程序次序规则:一个线程内书写在前的代码先执行,写在后面的代码后执行;
2.锁定规则:一个unLock操作先行发生于lock操作;
3.volatile规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
4.线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
5.线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
6.线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
7.传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
8.对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
三、volatile
上面聊了很多,接下来我们看下volatile,
volatile作用:
1.保证变量的可见性;
public class VolatileVisibility {
public static volatile int i =0;
public static void increase(){
i++;
}
}
针对于可见性我们分析下上面的代码,i被volatile修饰的情况下,i的任何改变都会反应到其他线程当中,但是当多线程同时调用increase()的方法时,会出现线程安全的问题,i++不是原子操作,该操作先读后写,分2步完成,如果第二个线程在第一个线程读取旧值和写回新值期间读取i的域值,那么第二个线程就会与第一个线程一起看到同一个值,并执行相同值的加1操作,这也就造成了线程安全失败;因此对于increase方法必须使用synchronized修饰,以便保证线程安全。
接下来我们再看一个例子:
public class VolatileDemo {
volatile boolean close;
public void close(){
close=true;
}
public void doWork(){
while (!close){
System.out.println("work...");
}
}
}
被volatile修饰的close字段,字段可以立即可见,保证当多个线程同时访问实例的时候,一个线程对close进行更新另外一个线程可立即获取到该字段变化以后的值;当写一个volatile变量时,JMM会把该线程对应的工作内存中的共享变量值刷新到主内存中,当读取一个volatile变量时,JMM会把该线程对应的工作内存置为无效,那么该线程将只能从主内存中重新读取共享变量。
对比下得出一个结论:volatile并不能保证原子性,只能保证可见性;
2.防止指令重排序;
明白这个要我们需要知道一个概念:内存屏障(Memory Barrier)是一个CPU指令,它的作用有两个,一是保证特定操作的执行顺序,二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。Memory Barrier的另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。
接下来我们看下我们最常见的单例模式:
public class SingletonDemo {
private volatile static SingletonDemo instance;
private SingletonDemo(){
System.out.println("Singleton has loaded");
}
public static SingletonDemo getInstance(){
if(instance==null){
synchronized (SingletonDemo.class){
if(instance==null){
instance=new SingletonDemo();
}
}
}
return instance;
}
}
这里我们思考下没有volatile的时候出现的问题:当然在单线程的情况下是不会出现问题,多线程下面会出现问题,首先这里我们要声明下volatile在这个地方只是防止指令重排,可见性是由锁实现的,接下来我们在分析为什么多线程会出现问题?
正常情况下初始化一个对象的过程如下:
1.分配内存空间;
2.初始化对象;
3.将内存空间的地址赋值给对象的引用;
当发生指令重排的时候可能会发生如下状况:
1.分配内存空间;
2.将内存空间的地址赋值给对象的引用;
3.初始化对象;
这个时候我们就来考虑下多线程情况下可能发生的问题喽,如果A线程正好初始化到了第(2)步的时候, 正好有其它线程B来获取这个对象, 那线程B能不能看到这个由A初始化但是还没初始化完毕的对象呢?答案是可能会看到这个未完全初始化的对象, 因为这里初始化的是一个共享变量,这个时候就会照成2种情况:
1.如果读到的是null,反而没问题了,接下来会等待锁,然后再次判断时不为null,最后返回单例。
2.如果读到的不是null,那么坏了,按逻辑它就直接return instance了,这个instance还没执行构造参数,去做事情的话,很可能就崩溃了。
注意:这个问题不容易出现理解下就好,不要和我一样调试到怀疑人生。。。。。
四、总结
JMM就是一组规则,这组规则为了处理在并发编程可能出现的线程安全问题,并提供了内置的(happen-before原则)以及其外部可使用的同步手段(synchronized/volatile等),保证程序在执行多线程时候的原子性,可见性以及有序性。
volatile不能保证原子性;
欢迎加群:438836709
欢迎关注公众号:
下一篇:spring IOC源码解读

面试(三)---volatile的更多相关文章
- [Java面试三]JavaWeb基础知识总结.
1.web服务器与HTTP协议 Web服务器 l WEB,在英语中web即表示网页的意思,它用于表示Internet主机上供外界访问的资源. l Internet上供外界访问的Web资源分为: • 静 ...
- Unity3D 面试三 ABCDE
说说AB两次面试: “金三银四” 三月份末又面试过两家:共和新路2989弄1号1001这家找了我半天,哇好漂亮的办公大楼!问了保安才知道,这个地址是小区地址.另一家也是创业公司面试我的自称是在腾讯做过 ...
- 三 volatile关键字
一:内存模型: 大家都知道,计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入.由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问 ...
- 面试并发volatile关键字时,我们应该具备哪些谈资?
提前发现更多精彩内容,请访问 个人博客 提前发现更多精彩内容,请访问 个人博客 提前发现更多精彩内容,请访问 个人博客 写在前面 在 可见性有序性,Happens-before来搞定 文章中,happ ...
- django面试三
1.Django. Flask.Tornado框架的比较? Django: 对于django,大而全的框架它的内部组件比较多,内部提供:ORM.Admin.中间件.Form.ModelForm.Ses ...
- Java并发编程的艺术(三)——volatile
1. 并发编程的两个关键问题 并发是让多个线程同时执行,若线程之间是独立的,那并发实现起来很简单,各自执行各自的就行:但往往多条线程之间需要共享数据,此时在并发编程过程中就不可避免要考虑两个问题:通信 ...
- Android面试三之Service
Service是什么 Service(服务)是一个没有用户界面的在后台运行执行耗时操作的应用组件.其他应用组件能够启动Service,并且当用户切换到另外的应用场景,Service将持续在后台运行.另 ...
- java面试总躲不过的并发(二):volatile原理 + happens-before原则
一.happens-before原则 同一个线程中的,前面的操作 happens-before 后续的操作.(即单线程内按代码顺序执行.但是,在不影响在单线程环境执行结果的前提下,编译器和处理器可以进 ...
- Java多线程学习(三)volatile关键字
转载请备注地址:https://blog.csdn.net/qq_34337272/article/details/79680693 系列文章传送门: Java多线程学习(一)Java多线程入门 Ja ...
随机推荐
- Android项目-高考作文项目架构(三)
上一篇我们讲到了, Http Json的功能的抽取. 如果我们请求的是一个列表的数据呢? 我们使用那个功能就不是很好. 因为一个列表, 还有很多其他功能(比如每个listView都需要setAdap ...
- java控制台输入带空格的字符串
java控制台输入带空格的字符串 Scanner sc = new Scanner(System.in); String str = sc.nextLine();
- Cracking the Coding Interview:: 寻找有环链表的环路起始节点
给定一个有环链表,实现一个算法返回环路的开头节点. 这个问题是由经典面试题-检测链表是否存在环路演变而来.这个问题也是编程之美的判断两个链表是否相交的扩展问题. 首先回顾一下编程之美的问题. 由于如果 ...
- oracle ebs应用产品安全性-数据访问权限集
定义 数据访问权限集是一个重要的.必须设定的系统配置文件选项.对具有相同科目表.日历和期间类型的分类帐及其所有平衡段值或管理段值的定义读写权限,系统管理员将其分配至不同的责任以控制不同的责任对分类帐数 ...
- Java进阶(二十五)Java连接mysql数据库(底层实现)
Java进阶(二十五)Java连接mysql数据库(底层实现) 前言 很长时间没有系统的使用java做项目了.现在需要使用java完成一个实验,其中涉及到java连接数据库.让自己来写,记忆中已无从搜 ...
- shell 常用正则表达式
"^\d+$" //非负整数(正整数 + 0) "^[0-9]*[1-9][0-9]*$" //正整数 "^((-\d+)|(0+))$" ...
- 使用Visual Studio创建图片精灵(Image Sprite)——Web Essential
原文:Creating Image Sprite in Visual Studio - Web Essential 译者注:有关图片精灵的信息请参阅http://baike.baidu.com/vie ...
- 【Linux 操作系统】Ubuntu 基础操作 基础命令 热键 man手册使用 关机 重启等命令使用
. : 关机, 如果将Linux默认运行等级设置为0, 系统将无法启动; -- : 多用户模式, 允许使用网络文件系统, 一般不使用图形界面登陆就是这种模式; -- : 多用户图形界面模式, 该模式下 ...
- 聊聊String
当我们最开始学习java的时候,老师会告诉我们字符串的比较需要用equals(); 真的是这样的吗? 我们看看下面的例子 public class TestString { public static ...
- 用LED灯和按键来模拟工业自动化设备的运动控制
开场白: 前面三节讲了独立按键控制跑马灯的各种状态,这一节我们要做一个机械手控制程序,这个机械手可以左右移动,最左边有一个开关感应器,最右边也有一个开关感应器.它也可以上下移动,最下面有一个开关感应器 ...