上一篇中,我们了解了Synchronized关键字,知道了它的基本使用方法,它的同步特性,知道了它与Java内存模型的关系,也明白了Synchronized可以保证“原子性”,“可见性”,“有序性”。今天我们来看看另外一个关键字Volatile,这也是极其重要的关键字之一。毫不夸张的说,面试的时候谈到Synchronized,必定会谈到Volatile。

一个小栗子

public class Main {
private static boolean isStop = false; public static void main(String[] args) {
new Thread(() -> {
while (true) {
if (isStop) {
System.out.println("结束");
return;
}
}
}).start(); try {
TimeUnit.SECONDS.sleep(3);
isStop = true;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

首先定义了一个全局变量:isStop=false。然后在main方法里面开了一个线程,里面是一个死循环,当isStop=true,打印出一句话,结束循环。主线程睡了三秒钟,把isStop改为true。

按道理来说,3秒钟后,会打印出一句话,并且结束循环。但是,出人意料的事情发生了,等了很久,这句话迟迟没有出现,也没有结束循环。

这是为什么?这又和内存模型有关了,由此可见,内存模型是多么重要,不光是Synchronized,还是这次的Volatile都和内存模型有关。

问题分析

我们再来看看内存模型:

线程的共享数据是存放在主内存的,每个线程都有自己的本地内存,本地内存是线程独享的。当一个线程需要共享数据,是先去本地内存中查找,如果找到的话,就不会再去主内存中找了,需要修改共享数据的话,是先把主内存的共享数据复制一份到本地内存,然后在本地内存中修改,再把数据复制到主内存。

如果把这个搞明白了,就很容易理解为什么会产生上面的情况了:

isStop是共享数据,放在了主内存,子线程需要这个数据,就把数据复制到自己的本地内存,此时isStop=false,以后直接读取本地内存就可以。主线程修改了isStop,子线程是无感知的,还是去本地内存中取数据,得到的isStop还是false,所以就造成了上面的情况。

Volatile与可见性

如何解决这个问题呢,只需要给isStop加一个Volatile关键字:

public class Main {
private static volatile boolean isStop = false; public static void main(String[] args) {
new Thread(() -> {
while (true) {
if (isStop) {
System.out.println("结束");
return;
}
}
}).start(); try {
TimeUnit.SECONDS.sleep(3);
isStop = true;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

运行,问题完美解决。

Volatile的作用:

  1. 当一个变量加了volatile关键字后,线程修改这个变量后,强制立即刷新回主内存。

  2. 如果其他线程的本地内存中有这个变量的副本,会强制把这个变量过期,下次就不能读取这个副本了,那么就只能去主内存取,拿到的数据就是最新的。

正是由于这两个原因,所以Volatile可以保证“可见性”

Volatile与有序性

指令重排的基本概念就不再阐述了,上两节内容已经介绍了指令重排的基本概念。

指令重排遵守的happens-before规则,其中有一条规则,就是Volatile规则:

被Volatile标记的不允许指令重排。

所以,Volatile可以保证“有序性”。

那内部是如何禁止指令重排的呢?在指令中插入内存屏障

内存屏障有四种类型,如下所示:

在生成指令序列的时候,会根据具体情况插入不同的内存屏障。

总结下,Volatile可以保证“可见性”,“有序性”

Volatile与单例模式

public class Main {
private static Main main; private Main() {
} public static Main getInstance() {
if (main != null) {
synchronized (Main.class) {
if (main != null) {
main = new Main();
}
}
}
return main;
}
}

这里比较经典的单例模式,看上去没什么问题,线程安全,性能也不错,又是懒加载,这个单例模式还有一个响当当的名字:DCL

但是实际上,还是有点问题的,问题就出在

  main = new Main();

这又和内存模型有关系了。执行这个创建对象会有3个步骤:

  1. 分配内存
  2. 执行构造方法
  3. 指向地址

说明创建对象不是原子性操作,但是真正引起问题的是指令重排。先执行2,还是先执行3,在单线程中是无所谓的,但是在多线程中就不一样了。如果线程A先执行3,还没来得及执行2,此时,有一个线程B进来了,发现main不为空了,直接返回main,然后使用返回出来的main,但是此时main还不是完整的,因为线程A还没有来得及执行构造方法。

所以单例模式得在定义变量的时候,加上Volatile,即:

public class Main {
private volatile static Main main; private Main() {
} public static Main getInstance() {
if (main == null) {
synchronized (Main.class) {
if (main == null) {
main = new Main();
}
}
}
return main;
}
}

这样就可以避免上面所述的问题了。

好了,这篇文章到这里主要内容就结束了,总结全文:Volatile可以保证“有序性”,“可见性”,但是无法保证“原子性”

题外话

嘿嘿,既然上面说的是主要内容结束了,就代表还有其他内容。

我们把文章开头的例子再次拿出来:

public class Main {
private static boolean isStop = false; public static void main(String[] args) {
new Thread(() -> {
while (true) {
if (isStop) {
System.out.println("结束");
return;
}
}
}).start(); try {
TimeUnit.SECONDS.sleep(3);
isStop = true;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

如果既想让子线程结束,又不想加Volatile关键字怎么办?这真的可以做到吗?当然可以。

public class Main {
private static boolean isStop = false; public static void main(String[] args) {
new Thread(() -> {
while (true) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (isStop) {
System.out.println("结束");
return;
}
}
}).start(); try {
TimeUnit.SECONDS.sleep(3);
isStop = true;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

在这里,我让子线程也睡了一秒,运行程序,发现子线程停止了。

public class Main {
private static boolean isStop = false; public static void main(String[] args) {
new Thread(() -> {
while (true) {
System.out.println("Hello");
if (isStop) {
System.out.println("结束");
return;
}
}
}).start(); try {
TimeUnit.SECONDS.sleep(3);
isStop = true;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

我把上面的让子线程睡一秒钟的代码替换成 System.out.println,竟然也成功让子线程停止了。

public class Main {
private static boolean isStop = false; public static void main(String[] args) {
new Thread(() -> {
while (true) {
Random random=new Random();
random.nextInt(150);
if (isStop) {
System.out.println("结束");
return;
}
}
}).start(); try {
TimeUnit.SECONDS.sleep(3);
isStop = true;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

这样也可以。

为什么呢?

因为JVM会尽力保证内存的可见性,即使这个变量没有加入Volatile关键字,主要CPU有时间,都会尽力保证拿到最新的数据。但是第一个例子中,CPU不停的在做着死循环,死循环内部就是判断isStop,没有时间去做其他的事情,但是只要给它一点机会,就像上面的 睡一秒钟,打印出一句话,生成一个随机数,这些操作都是比较耗时的,CPU就可能可以去拿到最新的数据了。不过和Volatile不同的是 Volatile是强制内存“可见性”,而这里是可能可以。

Volatile的那些事的更多相关文章

  1. 你真的了解volatile吗,关于volatile的那些事

    很早就接触了volatile,但是并没有特别深入的去研究她,只有一个朦胧的概念,就是觉得 用她来解决可见性的,但可见性又是什么呢? 最近经过查阅各种资料,并结合自己的思考和实践,对volatile有了 ...

  2. JAVA并发--volatile

    学过计算机组成原理的一定知道,为了解决内存速度跟不上CPU速度这个问题,在CPU的设计中加入了缓存机制,缓存的速度介于CPU和主存之间.在进行运算的时候,CPU将需要的数据映射一份在缓存中,然后直接操 ...

  3. Java并发控制机制详解

    在一般性开发中,笔者经常看到很多同学在对待java并发开发模型中只会使用一些基础的方法.比如Volatile,synchronized.像Lock和atomic这类高级并发包很多人并不经常使用.我想大 ...

  4. Lucene实战之初体验

    前言 最早做非结构化数据搜索时用的还是lucene.net,一直说在学习java的同时把lucene这块搞一搞,这拖了2年多了,终于开始搞这块了. 开发环境 idea2016.lucene6.0.jd ...

  5. Java并发控制机制

    在一般性开发中,笔者经常看到很多同学在对待java并发开发模型中只会使用一些基础的方法.比如volatile,synchronized.像Lock和atomic这类高级并发包很多人并不经常使用.我想大 ...

  6. java多线程有哪些实际的应用场景?

    多线程使用的主要目的在于: 1.吞吐量:你做WEB,容器帮你做了多线程,但是他只能帮你做请求层面的.简单的说,可能就是一个请求一个线程.或多个请求一个线程.如果是单线程,那同时只能处理一个用户的请求. ...

  7. Java多线程编程那些事:volatile解惑--转

    http://www.infoq.com/cn/articles/java-multi-thread-volatile/ 1. 前言 volatile关键字可能是Java开发人员“熟悉而又陌生”的一个 ...

  8. Java单例你所不知道的事,与Volatile关键字有染

    版权声明:本文为博主原创文章,未经博主允许不得转载. 如果问一个码农最先接触到的设计模式是什么,单例设计模式一定最差也是“之一”. 单例,Singleton,保证内存中只有一份实例对象存在. 问:为什 ...

  9. Java的多线程机制系列:不得不提的volatile及指令重排序(happen-before)

    一.不得不提的volatile volatile是个很老的关键字,几乎伴随着JDK的诞生而诞生,我们都知道这个关键字,但又不太清楚什么时候会使用它:我们在JDK及开源框架中随处可见这个关键字,但并发专 ...

随机推荐

  1. C#处理JavaScript引擎

    概述 通常JavaScript在Web浏览器执行展现特定效果,C#也可以后台执行JavaScript里面方法:C#通常调用第三方DLL方式:MsieJavaScriptEngine或者JavaScri ...

  2. java servlet简述

    概述 servlet就相当于一个java小程序,其作用主要是为了接收并响应客户端的请求 入门 1.新建工程并新建个类 2.重写servlet的方法(重要的是service方法) 3.部署工程 a.在w ...

  3. Codeforces 1070J Streets and Avenues in Berhattan dp

    Streets and Avenues in Berhattan 我们首先能发现在最优情况下最多只有一种颜色会分别在行和列, 因为你把式子写出来是个二次函数, 在两端取极值. 然后我们就枚举哪个颜色会 ...

  4. 史上最全的select加锁分析(Mysql)

    引言 大家在面试中有没遇到面试官问你下面六句Sql的区别呢 select * from table where id = ? select * from table where id < ? s ...

  5. [Log函数]C++log函数使用

    先引入头文件#include<cmath> 以e为底:log(exp(n)) 以10为底:log10(n) 以m为底:log(n)/log(m)

  6. ISP PIPLINE (九_1) Denoise 之 space domain denoise

    1.空间域噪声类型 1.gauss+possion 2.椒盐噪声(dpc处理已经处理了) 去除空域噪声有哪些方法? 空域噪声一般的思想是对某pixel邻域的pixels进行加权平均. 比如 1.高斯降 ...

  7. CentOS Linux搭建SVN服务器

    Linux系统:CentOS 安装步骤如下: 1.yum install subversion 2.输入rpm -ql subversion查看安装位置,如下图: 可以看到 svn在bin目录下生成了 ...

  8. C++进阶:新人易入的那些坑 --1.常量、常指针和指针常量

    声明:以下内容B站/Youtube学习笔记,https://www.youtube.com/user/BoQianTheProgrammer/ Advanced C++. /* why use con ...

  9. SpringBoot几种定时任务的实现方式

    定时任务实现的几种方式: Timer:这是java自带的java.util.Timer类,这个类允许你调度一个java.util.TimerTask任务.使用这种方式可以让你的程序按照某一个频度执行, ...

  10. LeetCode 单链表专题 (一)

    目录 LeetCode 单链表专题 <c++> \([2]\) Add Two Numbers \([92]\) Reverse Linked List II \([86]\) Parti ...