一、前言 

     最近去成都玩了一圈,整体感觉还不错,辞职以后工作找的也很顺利,随着年龄增加自己也考虑定居和个人长期发展的问题,反正乱七八糟的事,总之需要好好屡屡思路,不能那么着急下定论,当然我对下份工作也是有所期望的,不扯了开始我们今天主题吧。

二、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的更多相关文章

  1. [Java面试三]JavaWeb基础知识总结.

    1.web服务器与HTTP协议 Web服务器 l WEB,在英语中web即表示网页的意思,它用于表示Internet主机上供外界访问的资源. l Internet上供外界访问的Web资源分为: • 静 ...

  2. Unity3D 面试三 ABCDE

    说说AB两次面试: “金三银四” 三月份末又面试过两家:共和新路2989弄1号1001这家找了我半天,哇好漂亮的办公大楼!问了保安才知道,这个地址是小区地址.另一家也是创业公司面试我的自称是在腾讯做过 ...

  3. 三 volatile关键字

    一:内存模型: 大家都知道,计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入.由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问 ...

  4. 面试并发volatile关键字时,我们应该具备哪些谈资?

    提前发现更多精彩内容,请访问 个人博客 提前发现更多精彩内容,请访问 个人博客 提前发现更多精彩内容,请访问 个人博客 写在前面 在 可见性有序性,Happens-before来搞定 文章中,happ ...

  5. django面试三

    1.Django. Flask.Tornado框架的比较? Django: 对于django,大而全的框架它的内部组件比较多,内部提供:ORM.Admin.中间件.Form.ModelForm.Ses ...

  6. Java并发编程的艺术(三)——volatile

    1. 并发编程的两个关键问题 并发是让多个线程同时执行,若线程之间是独立的,那并发实现起来很简单,各自执行各自的就行:但往往多条线程之间需要共享数据,此时在并发编程过程中就不可避免要考虑两个问题:通信 ...

  7. Android面试三之Service

    Service是什么 Service(服务)是一个没有用户界面的在后台运行执行耗时操作的应用组件.其他应用组件能够启动Service,并且当用户切换到另外的应用场景,Service将持续在后台运行.另 ...

  8. java面试总躲不过的并发(二):volatile原理 + happens-before原则

    一.happens-before原则 同一个线程中的,前面的操作 happens-before 后续的操作.(即单线程内按代码顺序执行.但是,在不影响在单线程环境执行结果的前提下,编译器和处理器可以进 ...

  9. Java多线程学习(三)volatile关键字

    转载请备注地址:https://blog.csdn.net/qq_34337272/article/details/79680693 系列文章传送门: Java多线程学习(一)Java多线程入门 Ja ...

随机推荐

  1. VC++读取图像RGB值

    代码: #include <iostream> #include <fstream> #include <string> #include <windows. ...

  2. Android Wear开发

    Android Wear从2014年3月发布到现在已经从1.0发展到2.0(目前还没正式发布).其产品定位也发化了巨大变化,因为Android Wear 1.0通讯方式只有蓝牙,限定了系统,比较依赖手 ...

  3. Shell入门之概念

    1.一切皆是文件: 在bash Shell 中一切皆是文件,不管是我们认为的文本文件,还是那些文件夹的东西,在这里都是文件,Linux只管比特和字节流,而不关心他们最终组成了什么格式,这些工作交给在L ...

  4. HTML入门笔记案例展示(2)

    结上一篇(1)http://blog.csdn.net/qq_32059827/article/details/51399288 6.超链接(重要): 超链接标签 就是 a 标签 <!-- 超链 ...

  5. H5学习之旅-H5与Php交互(12)

    1.首先介绍PHP开发环境的搭建 ,在Google搜apachefriends,会有xampp的下载链接,这个工具集成了apache的很多服务 2.搭建php的编辑环境,选取eclipse安装php插 ...

  6. Android APP新的“优雅”退出方式--EventBus大显身手

    最近在研究eventBus..很多小伙伴不知道他有什么用.. 前篇介绍了EventBus的基本使用 这里简单举一个例子,就是退出APP 转载请注明出处:http://blog.csdn.net/win ...

  7. 01_MUI之Boilerplate中:HTML5示例,动态组件,自定义字体示例,自定义字体示例,图标字体示例

     1安装HBuilder5.0.0,安装后的界面截图如下: 2 按照https://www.muicss.com/docs/v1/css-js/boilerplate-html中的说明,创建上图的 ...

  8. android动画介绍--Animation 实现loading动画效果

    Animation的使用方法并不难.这里简单的介绍一下使用方法. 先看效果图: 效果还是不错的吧. 下面来看看使用方法. 动画效果是通过Animation来实现的,一共有四种,分别为: AlphaAn ...

  9. 关于App启动加载广告页面思路

    需求 很多app(如淘宝.美团等)在启动图加载完毕后,还会显示几秒的广告,一般都有个跳过按钮可以跳过这个广告,有的app在点击广告页之后还会进入一个广告页面,点击返回进入首页.虽然说这个广告页面对用户 ...

  10. C++11特性 - Smart Pointers 智能指针

    已经有成千上万的文章讨论这个问题了,所以我只想说:现在能使用的,带引用计数,并且能自动释放内存的智能指针包括以下几种:         unique_ptr: 如果内存资源的所有权不需要共享,就应当使 ...