这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战
volatile是java虚拟机提供的轻量级的同步机制
1.保证可见性、2.不保证原子性、3.禁止指令重排
1.保证可见性
当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改的值
当不添加volatile关键字时示例:
package com.jian8.juc;
import java.util.concurrent.TimeUnit;
/**
* 1验证volatile的可见性
* 1.1 如果int num = 0,number变量没有添加volatile关键字修饰
* 1.2 添加了volatile,可以解决可见性
*/
public class VolatileDemo {
public static void main(String[] args) {
visibilityByVolatile();//验证volatile的可见性
}
/**
* volatile可以保证可见性,及时通知其他线程,主物理内存的值已经被修改
*/
public static void visibilityByVolatile() {
MyData myData = new MyData();
//第一个线程
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t come in");
try {
//线程暂停3s
TimeUnit.SECONDS.sleep(3);
myData.addToSixty();
System.out.println(Thread.currentThread().getName() + "\t update value:" + myData.num);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}, "thread1").start();
//第二个线程是main线程
while (myData.num == 0) {
//如果myData的num一直为零,main线程一直在这里循环
}
System.out.println(Thread.currentThread().getName() + "\t mission is over, num value is " + myData.num);
}
}
class MyData {
// int num = 0;
volatile int num = 0;
public void addToSixty() {
this.num = 60;
}
}
输出结果:
thread1 come in
thread1 update value:60
//线程进入死循环
当我们加上volatile关键字后,volatile int num = 0;
输出结果为:
thread1 come in
thread1 update value:60
main mission is over, num value is 60 //程序没有死循环,结束执行。
2. 不保证原子性
原子性:不可分割、完整性,即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割,需要整体完整,要么同时成功,要么同时失败
验证示例(变量添加volatile关键字,方法不添加synchronized):
package com.jian8.juc;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 1验证volatile的可见性
* 1.1 如果int num = 0,number变量没有添加volatile关键字修饰
* 1.2 添加了volatile,可以解决可见性
*
* 2.验证volatile不保证原子性
* 2.1 原子性指的是什么
* 不可分割、完整性,即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割,需要整体完整,要么同时成功,要么同时失败
*/
public class VolatileDemo {
public static void main(String[] args) {
// visibilityByVolatile();//验证volatile的可见性
atomicByVolatile();//验证volatile不保证原子性
}
/**
* volatile可以保证可见性,及时通知其他线程,主物理内存的值已经被修改
*/
//public static void visibilityByVolatile(){}
/**
* volatile不保证原子性
* 以及使用Atomic保证原子性
*/
public static void atomicByVolatile(){
MyData myData = new MyData();
for(int i = 1; i <= 20; i++){
new Thread(() ->{
for(int j = 1; j <= 1000; j++){
myData.addSelf();
myData.atomicAddSelf();
}
},"Thread "+i).start();
}
//等待上面的线程都计算完成后,再用main线程取得最终结果值
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
while (Thread.activeCount()>2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"\t finally num value is "+myData.num);
System.out.println(Thread.currentThread().getName()+"\t finally atomicnum value is "+myData.atomicInteger);
}
}
class MyData {
// int num = 0;
volatile int num = 0;
public void addToSixty() {
this.num = 60;
}
public void addSelf(){
num++;
}
AtomicInteger atomicInteger = new AtomicInteger();
public void atomicAddSelf(){
atomicInteger.getAndIncrement();
}
}
执行三次结果为:
//1.
main finally num value is 19580
main finally atomicnum value is 20000
//2.
main finally num value is 19999
main finally atomicnum value is 20000
//3.
main finally num value is 18375
main finally atomicnum value is 20000 //num并没有达到20000
3.禁止指令重排
有序性:在计算机执行程序时,为了提高性能,编译器和处理器常常会对**==指令做重排序==**,一般分以下三种
graph LR
源代码 --> id1["编译器优化的重排"]
id1 --> id2[指令并行的重排]
id2 --> id3[内存系统的重排]
id3 --> 最终执行的指令
style id1 fill:#ff8000;
style id2 fill:#fab400;
style id3 fill:#ffd557;
单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。 处理器在进行重排顺序是必须要考虑指令之间的**==数据依赖性==** ==多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性时无法确定的,结果无法预测==
重排代码实例:
声明变量:int a,b,x,y=0
线程1 线程2
x = a; y = b;
b = 1; a = 2;
结 果 x = 0 y=0
如果编译器对这段程序代码执行重排优化后,可能出现如下情况:
线程1 线程2
b = 1; a = 2;
x= a; y = b;
结 果 x = 2 y=1
这个结果说明在多线程环境下,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的
volatile实现禁止指令重排,从而避免了多线程环境下程序出现乱序执行的现象
==内存屏障==(Memory Barrier)又称内存栅栏,是一个CPU指令,他的作用有两个:
- 保证特定操作的执行顺序。
- 保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。 由于编译器和处理器都能执行指令重排优化。如果在之零件插入一i奥Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排顺序,也就是说==通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化==。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。
graph TB
subgraph
bbbb["对Volatile变量进行读操作时,<br>回在读操作之前加入一条load屏障指令,<br>从内存中读取共享变量"]
ids6[Volatile]-->red3[LoadLoad屏障]
red3-->id7["禁止下边所有普通读操作<br>和上面的volatile读重排序"]
red3-->red4[LoadStore屏障]
red4-->id9["禁止下边所有普通写操作<br>和上面的volatile读重排序"]
red4-->id8[普通读]
id8-->普通写
end
subgraph
aaaa["对Volatile变量进行写操作时,<br>回在写操作后加入一条store屏障指令,<br>将工作内存中的共享变量值刷新回到主内存"]
id1[普通读]-->id2[普通写]
id2-->red1[StoreStore屏障]
red1-->id3["禁止上面的普通写和<br>下面的volatile写重排序"]
red1-->id4["Volatile写"]
id4-->red2[StoreLoad屏障]
red2-->id5["防止上面的volatile写和<br>下面可能有的volatile读写重排序"]
end
style red1 fill:#ff0000;
style red2 fill:#ff0000;
style red4 fill:#ff0000;
style red3 fill:#ff0000;
style aaaa fill:#ffff00;
style bbbb fill:#ffff00;
你在那些地方用过volatile
当普通单例模式在多线程情况下:
public class SingletonDemo {
private static SingletonDemo instance = null;
private SingletonDemo() {
System.out.println(Thread.currentThread().getName() + "\t 构造方法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();
}
}
}
其构造方法在一些情况下会被执行多次
解决方式: 单例模式DCL代码 DCL (Double Check Lock双端检锁机制)在加锁前和加锁后都进行一次判断
public static SingletonDemo getInstance() {
if (instance == null) {
synchronized (SingletonDemo.class) {
if (instance == null) {
instance = new SingletonDemo();
}
}
}
return instance;
}
大部分运行结果构造方法只会被执行一次,但指令重排机制会让程序很小的几率出现构造方法被执行多次
==DCL(双端检锁)机制不一定线程安全==,原因时有指令重排的存在,加入volatile可以禁止指令重排
原因是在某一个线程执行到第一次检测,读取到instance不为null时,instance的引用对象可能==没有完成初始化==。instance=new SingleDemo();可以被分为一下三步(伪代码):
memory = allocate();//1.分配对象内存空间
instance(memory); //2.初始化对象
instance = memory; //3.设置instance执行刚分配的内存地址,此时instance!=null
步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化时允许的,如果3步骤提前于步骤2,但是instance还没有初始化完成
但是指令重排只会保证串行语义的执行的一致性(单线程),但并不关心多线程间的语义一致性。
==所以当一条线程访问instance不为null时,由于instance示例未必已初始化完成,也就造成了线程安全问题。==
单例模式volatile代码 为解决以上问题,可以将SingletongDemo实例上加上volatile
private static volatile SingletonDemo instance = null;