一.谈谈对volatile的理解
volatile是java虚拟机提供的轻量级的同步机制
保证可见性、不保证原子性、禁止指令重排
1.可见性理解:所有线程存放都是主内存的副本(比如某个变量值为25),t1线程的工作内存发生改变(值25改为37),写会主内存中,及时通知其他线程t2,t3更新最新的主内存数据(37),达到数据一致性,这种及时通知其他线程俗称可见性
2.可见性的代码验证
** * 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() + " come in"); try { //线程暂停3s TimeUnit.SECONDS.sleep(3); myData.addToSixty(); System.out.println(Thread.currentThread().getName() + " 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() + " 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.不保证原子性
原子性:不可分割、完整性,即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割,需要整体完整,要么同时成功,要么同时失败
代码验证
Mydata mydata = new Mydata(); System.out.println("改变之前,主线程的获取值为:"+mydata.data); // new Thread(()->{ // try { // Thread.sleep(3000); // } catch (InterruptedException e) { // e.printStackTrace(); // } // mydata.addSixty(); // System.out.println("线程修改后的,值为:"+mydata.data); // }).start(); //等待会 // while (mydata.data==0){ // // } for (int i = 0; i < 20; i++) { new Thread(()->{ for (int i1 = 0; i1 < 1000; i1++) { mydata.add(); } System.out.println("线程修改后的,值为:"+mydata.data); },String.valueOf(i)).start(); } Thread.sleep(3000); // while (Thread.activeCount()>2){ // Thread.yield(); // } System.out.println("20个线程进行++操作后,主线程的获取值为:"+mydata.data); } } class Mydata{ volatile int data=0; public void addSixty(){ this.data=60; } public void add(){ this.data++; } }
输出打印:
20个线程进行++操作后,主线程的获取值为:19772
发现得到的值不是20000
(1)为什么保证不了原子性
java字节码(https://blog.csdn.net/hao707822882/article/details/26974073)
public void add();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field data:I ======》从对象中获取字段
5: iconst_1 ========》将int类型常量1压入栈
6: iadd =====》加
7: putfield #2 // Field data:I =====》设置对象中字段的值
10: return
原因:java一行i++操作,在jvm底层需要执行四行字节码指令,线程1可能在改变值后,在往主内存更新时(线程太快了),某个步骤被挂起,线程2,3来写入,导致线程没有更新成功,写丢失
3.如何保证原子性呢
(1)不使用synchronized,使用原子类AtomicInteger进行++操作
4.禁止指令重排序
==多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性时无法确定的,结果无法预测==
重排代码实例:
声明变量: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上的线程都能读取到这些数据的最新版本。
3、你在那些地方用过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();
}
}
}
其构造方法在一些情况下会被执行多次
解决方式:
-
单例模式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;