Volatile的那些事
上一篇中,我们了解了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的作用:
- 当一个变量加了volatile关键字后,线程修改这个变量后,强制立即刷新回主内存。 
- 如果其他线程的本地内存中有这个变量的副本,会强制把这个变量过期,下次就不能读取这个副本了,那么就只能去主内存取,拿到的数据就是最新的。 
正是由于这两个原因,所以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个步骤:
- 分配内存
- 执行构造方法
- 指向地址
说明创建对象不是原子性操作,但是真正引起问题的是指令重排。先执行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的那些事的更多相关文章
- 你真的了解volatile吗,关于volatile的那些事
		很早就接触了volatile,但是并没有特别深入的去研究她,只有一个朦胧的概念,就是觉得 用她来解决可见性的,但可见性又是什么呢? 最近经过查阅各种资料,并结合自己的思考和实践,对volatile有了 ... 
- JAVA并发--volatile
		学过计算机组成原理的一定知道,为了解决内存速度跟不上CPU速度这个问题,在CPU的设计中加入了缓存机制,缓存的速度介于CPU和主存之间.在进行运算的时候,CPU将需要的数据映射一份在缓存中,然后直接操 ... 
- Java并发控制机制详解
		在一般性开发中,笔者经常看到很多同学在对待java并发开发模型中只会使用一些基础的方法.比如Volatile,synchronized.像Lock和atomic这类高级并发包很多人并不经常使用.我想大 ... 
- Lucene实战之初体验
		前言 最早做非结构化数据搜索时用的还是lucene.net,一直说在学习java的同时把lucene这块搞一搞,这拖了2年多了,终于开始搞这块了. 开发环境 idea2016.lucene6.0.jd ... 
- Java并发控制机制
		在一般性开发中,笔者经常看到很多同学在对待java并发开发模型中只会使用一些基础的方法.比如volatile,synchronized.像Lock和atomic这类高级并发包很多人并不经常使用.我想大 ... 
- java多线程有哪些实际的应用场景?
		多线程使用的主要目的在于: 1.吞吐量:你做WEB,容器帮你做了多线程,但是他只能帮你做请求层面的.简单的说,可能就是一个请求一个线程.或多个请求一个线程.如果是单线程,那同时只能处理一个用户的请求. ... 
- Java多线程编程那些事:volatile解惑--转
		http://www.infoq.com/cn/articles/java-multi-thread-volatile/ 1. 前言 volatile关键字可能是Java开发人员“熟悉而又陌生”的一个 ... 
- Java单例你所不知道的事,与Volatile关键字有染
		版权声明:本文为博主原创文章,未经博主允许不得转载. 如果问一个码农最先接触到的设计模式是什么,单例设计模式一定最差也是“之一”. 单例,Singleton,保证内存中只有一份实例对象存在. 问:为什 ... 
- Java的多线程机制系列:不得不提的volatile及指令重排序(happen-before)
		一.不得不提的volatile volatile是个很老的关键字,几乎伴随着JDK的诞生而诞生,我们都知道这个关键字,但又不太清楚什么时候会使用它:我们在JDK及开源框架中随处可见这个关键字,但并发专 ... 
随机推荐
- C++ STL容器总结
			1. STL 容器 1. 按种类划分 顺序容器( sequence containers):是一种各元素之间有顺序关系的线性表,是一种线性结构的可序群集.顺序性容器中的每个元素均有固定的位 ... 
- go time模块
			package main import ( "fmt" "time") func test() { time.Sleep(time.Millisecond * ... 
- 配置plsql远程连接oracle数据库
			1.首先安装plsql工具.(plsql如何安装就不说明了) 2.在plsql安装目录下新建“network”文件夹,在network文件夹中新建“admin”文件夹,在admin中新建“tnsnam ... 
- keepalived安装与配置,组建高可用服务器
			一.准备环境 linux系统:CentOS7 keepalived版本:keepalived-1.3.5.tar.gz keepalived下载地址:http://www.keepalived.org ... 
- 如何明确区分代码中的1和l
			如poly1d 单独将其复制到记事本,然后按ctrl+F,输入要查找的对象,数字1或者小写字母l,找到的对象会已高亮表示,所以就可以确定了高亮表示的是数字1还是字母l. 
- jar文件内lib引用的jar插件修改后更新
			打包的java服务在第三方jar进行修改后,要更新线上的jar包时,直接替换原有lib引用的jar文件,会造成服务起不来, 可在本地clean install之后,用线上的classes文件夹替换本地 ... 
- Object冷知识
			Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__ 语法:Object.create(proto, [propertiesObject]) prop ... 
- Loading Data into a Table;MySQL从本地向数据库导入数据
			在localhost中准备好了一个test数据库和一个pet表: mysql> SHOW DATABASES; +--------------------+ | Database | +---- ... 
- Git 通过ssh 配置基于Host的差异配置
			Host gitlab.xxx.com HostName gitlab.xxx.com User user IdentityFile xxx\.ssh\id_rsa Host github.com H ... 
- W3C的标准到底是啥?
			1.图片的alt="" 属性必须每张图片都加上,而且对齐属性用CSS来定义.不加不能通过XHTML 1.0的验证. 2.每个文档必须加上DTD声明. a) !DOCTYPE htm ... 
