一、volatile是什么?
volatile是一种轻量级的同步机制
二、volatile的三种特性?
1.保证可见性
2.不保证原子性
3.禁止指令重排
三、JMM(内存模型)的概念
JMM简单介绍
在说volatile之前,我们需要知道JMM。JMM是什么呢,JMM表示JAVA内存模型,他是一种抽象的概念,它表示一种约定,规范,实际上并不存在。
java在执行指令的时候,会涉及到对数据的读写,我们知道,数据是放在主存中的,当程序在运行的过程中,会将运行所需要的数据从主存复制一份到CPU的高速缓存当中,
这样CPU进行就可以直接从它的高速缓存读取数据并且向其中写入数据,当计算运行结束之后,将高速缓存的数据同步到主存中,
比如对于
i = i + 1;
对于这个例子来说,当线程执行到这儿的时候,先从主存中获取i的值,然后拷贝一份数据扔到高速缓存中,然后CPU将对变量i进行加1的操作,将结果写入高速缓存,最后,高速缓存将最终的结果同步到主存,对于单线程来说,这样是不存在问题的,取数、复制、计算、同步。
但是对于。多线程的情况下,比如两个线程,期望这两个线程执行完的结果使得i变成2,但是事实可能并没有我们想到这么顺利。每个CPU有自己的高速缓存,如果这两个线程被不同的CPU执行,此时可能会出现这样一种情况,线程都从主存1、2取出原始数据0,经过一番操作,线程1会将i变成1,然后同步到主存,线程2也是这样,这个时候结果就是1,而不是2了。
也就是说,如果一个变量在多个CPU中都存在缓存,那么就可能存在缓存不一致的问题。
为了解决这个问题,java使用锁机制来处理,那么对于并发编程来说,一般会遇见原子性问题,可见性问题,有序性问题三个问题。JMM对这种问题处理如下。
JMM关于同步的规定
1、线程解锁前,必须把共享变量的值刷新主内存
2、线程加锁前,必须读取主内存的最新值到自己的工作内存
3、加锁解锁是同一把锁
为了继续说明,和volatile穿插。先介绍一下原子性问题,可见性问题,有序性问题这三个概念。
先有一个概念:volatile不保证原子性,synchronized都保证。 可见性就是一种多线程信息的同步机制。
可见性
t1、t2、t3线程都是从主内存拿出变量,在自己的工作内存中做一个变量副本,然后操作副本的值,修改之后,在同步到主内存,但是 t1修改之后同步到主内存后,如何保证t2和t3和主内存保持一致呢,这个机制就是可见性。
举个例子
现在要对Data里面的a元素加一,期望主线程和其他一个线程都获取到初始值之后,由其他线程改变a的之后,然后主线程里面获取到,期望的场景就是这样
public class VolatileDemo {
public static void main(String[] args) {
Data data = new Data();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " is coming");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
data.addOne();
System.out.println(Thread.currentThread().getName() + " has updated...");
},"thread1").start();
while (data.a == 0) {
// looping
}
System.out.println(Thread.currentThread().getName() + " job is done...");
}
}
class Data{
int a = 0;
void addOne(){
this.a += 1;
}
}
上面的这个代码就是验证了可见性,对于新的线程thread1和主线程来说,都是把data的值拷贝到自己线程的工作区间去操作的,但是thread1线程操作后,已经把值更新成1,并且写回主内存了,但是没有保证可见性,那么main线程的值一直就是0,所以这个程序会一直走while去判断0。所以就是不可见。那么加上了volatile之后,就可以验证可见性。volatile的就可以解决可见性的问题。
volatile int a = 0;
现在就是只要有一个线程修改了值马上刷新同步回主内存,同时对其他线程可见。
原子性
原子性是指一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着“同生共死”的感觉。在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。
volatile已经说了是不支持原子操作的。
为了检验volatile不能保证原子性,举一个例子,就是20个线程做i++操作,循环1000次,最后的i结果应该是20000。 下面这个结果有可能是20000。但是实际上很难,以此来验证volatile不能保证原子性。
class Data {
volatile int num = 0;
public void addSelf(){
num++;
}
}
public class VolatileDemo {
public static void main(String[] args) {
atomicByVolatile();//验证volatile不保证原子性
}
public static void atomicByVolatile() {
Data myData = new Data();
for (int i = 1; i <= 20; i++) {
new Thread(() -> {
for (int j = 1; j <= 1000; j++) {
myData.addSelf();
}
}, "Thread " + i).start();
}
//等待上面的线程都计算完成后,再用main线程取得最终结果值
//设置为2的原因是,默认有两个线程,一个是主线程,另外一个是gc线程
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + " finally num value is " + myData.num);
}
}
打印输出
main finally num value is 18622
对于这20个线程来说,线程t1从主内存拿到i=0的值,改为t1的时候,正常情况下t2或者t3都应该立即同步主内存的值,若此时t2或者t3发生了阻塞加塞,那么t2或者t3就会把自己的值去同步到主内存,从而发生写覆盖。
那volatile不保证原子性怎么解决呢,一般有两种方法
第一种:方法加synchronized
第二种:加Atomic
用第二种方法来尝试解决这个问题
class Data {
volatile int num = 0;
public void addSelf(){
num++;
}
AtomicInteger atomicInteger = new AtomicInteger();
public void atomicAddSelf(){
atomicInteger.getAndIncrement();
}
}
public class VolatileDemo {
public static void main(String[] args) {
atomicByVolatile();//验证volatile不保证原子性
}
/**
* volatile不保证原子性
* 以及使用Atomic保证原子性
*/
public static void atomicByVolatile() {
Data myData = new Data();
for (int i = 1; i <= 20; i++) {
new Thread(() -> {
for (int j = 1; j <= 1000; j++) {
myData.addSelf();
myData.atomicAddSelf();
}
}, "Thread " + i).start();
}
//等待上面的线程都计算完成后,再用main线程取得最终结果值
//设置为2的原因是,默认有两个线程,一个是主线程,另外一个是gc线程
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + " finally num value is " + myData.num);
System.out.println(Thread.currentThread().getName()+" finally atomicnum value is "+myData.atomicInteger);
}
}
输出结果
main finally num value is 19882
main finally atomicnum value is 20000
AtomicInteger表示带有原子性的int类型,调用getAndIncrement()表示++。
所以,多线程环境下不要++。用getAndIncrement()。
那么Atomic为什么能保证原子性呢?底层是什么呢?这个就涉及到CAS了。具体看CAS这部分CAS浅析
有序性
volatile禁止指令重排,有序性就是指令重排
public class ReSortDemo {
int a = 0;
boolean flag = false;
public void method1(){
a = 1; //语句一
flag = true; //语句二
}
//多线程环境中线程交替执行,由于编译器优化重排的存在
//两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测
public void method2(){
if(flag){
a = a + 5;
System.out.println("****reValue:" + a);
}
}
}
在单线程环境下先走method1再走method2,结果是6,但是在多线程环境下,语句一和语句二在个线程的执行顺序是被优化的,若先走语句一,再走语句二,是6,但是先走语句二,还没有走到语句一之后,先走了method2,那么结果就是5。volatile就可以防止这种情况,也就是禁止指令重排。
volatile 实现禁止指令重排序的优化,从而避免了多线程环境下程序出现乱序的现象
volatile使用的例子
那么volatile在多线程使用使用的例子呢,那就是单例模式
那么多线程条件下如何构建单例模式呢? 单机版情况下,多线程单例模式,下面这个代码是有问题的
public class SingletonDemo {
private static SingletonDemo instance = null;
private SingletonDemo() {
System.out.println(Thread.currentThread().getName() + " 构造方法SingletonDemo()");
}
public static SingletonDemo getInstance() {
if (instance == null) {
instance = new SingletonDemo();
}
return instance;
}
public static void main(String[] args) {
//构造方法只会被执行一次
// System.out.println(getInstance() == getInstance());
// System.out.println(getInstance() == getInstance());
// System.out.println(getInstance() == getInstance());
//构造方法会在一些情况下执行多次
for (int i = 0; i < 10; i++) {
new Thread(() -> {
SingletonDemo.getInstance();
}, "Thread " + i).start();
}
}
}
上面这个代码每次创建的单例模式是有问题的,可能创建出来多个,我们可以对方法getInstance()加synchronized,但是这样的话,会对整个方法都锁住,并不是很理想。真正需要控制的就是里面new这一行,所以有一个DCL模式的单例模式,也就是双重检验锁的单例模式。也就是前后都判断一次。
// DCL Double check lock 双重检验锁,加锁前后都进行判断
public static SingletonDemo getInstance() {
if (instance == null) {
synchronized (SingletonDemo.class) {
if (instance == null) {
instance = new SingletonDemo();
}
}
}
return instance;
}
但是,DCL不一定保证线程安全,因为多线程存在指令重排,指令重排是什么呢,先看一个概念,内存屏障。
内存屏障(Memory Barrier)又称内存栅栏,是一个 CPU 指令,他的作用有两个:
- 保证特定操作的执行顺序
- 保证某些变量的内存可见性(利用该特性实现 volatile 的内存可见性)
由于编译器处理器都能执行指令重排序优化,如果在指令间插入一条 Memory Barrier 则会告诉编译器和 CPU,不管什么指令都不能个这条 Memory Barrier 指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后执行重排序优化。内存屏障另一个作用是强制刷出各种 CPU 缓存数据,因此任何 CPU 上的线程都能读取到这些数据的最新版本。
下面是保守策略下,volatile写插入内存屏障后生成的指令序列示意图:
对于这个单例模式的例子来说:指令重排只会保证串行语义保证一致性(单线程),并不会关心多线程条件下的语义一致性
所以当一条线程访问instance不加null时候,由于instance实例未必已经初始化完成,所以也就造成了线程安全性问题。
所以需要加volatile关键字去禁止指令重排。
public class SingletonDemo {
private static volatile SingletonDemo instance = null;
private SingletonDemo() {
System.out.println(Thread.currentThread().getName() + " 构造方法SingletonDemo()");
}
// DCL Double check lock 双重检验锁,加锁前后都进行判断
public static SingletonDemo getInstance() {
if (instance == null) {
synchronized (SingletonDemo.class) {
if (instance == null) {
instance = new SingletonDemo();
}
}
}
return instance;
}
public static void main(String[] args) {
//构造方法只会被执行一次
// System.out.println(getInstance() == getInstance());
// System.out.println(getInstance() == getInstance());
// System.out.println(getInstance() == getInstance());
//构造方法会在一些情况下执行多次
for (int i = 0; i < 10; i++) {
new Thread(() -> {
SingletonDemo.getInstance();
}, "Thread " + i).start();
}
}
}
线程的安全性保证
工作内存和主内存之间的同步延迟现象导致的可见性问题,可以通过volatile和synchronized关键字来解决,他们都可以使得一个线程修改变量的值后立即对其他的值可见。 关于指令重排导致的可见性和有序性问题,可以用volatile关键字解决,因为volatile的另一个作用就是禁止指令重排。