volatile
第01章 volatile的作用
1.线程间可见性
说明: 一个线程对共享变量值的修改,能够及时的被其他线程看到。
2.共享变量可见性实现的原理
线程1对共享变量的修改要想被线程2及时看到,必须经过如下2个步骤:
- 把工作内存1中更新过的共享变量刷新到主内存中
- 将主内存中最新的共享变量的值更新到工作内存2中
代码演示
import java.util.concurrent.TimeUnit;
public class T01_HelloVolatile {
/*volatile*/ boolean running= true;
void m() {
System.out.println("m start");
while (running) {
}
System.out.println("m end!");
}
public static void main(String[] args) {
T01_HelloVolatile t=new T01_HelloVolatile();
new Thread(t::m, "t1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.running=false;
}
}
效果图
加上volatile关键字后的效果图
volatile boolean running= true;
注意事项:
- 很多的执行语句有对内存进行同步的过程 例如
System.out.println("hello");
在执行的过程中,内存与本地缓存做同步, 很有可能在同步过程中又得到了新的volatile
的共享变量的值 (此处打印语句中使用了同步锁synchronized )
2.禁止指令重排序
带着一个问题: 美团面试题: DCL单例要不要加volatile? 问题的来源在单例中
cpu的乱序执行概念
cpu在进行读等待的同时执行命令, 是cpu乱序的根源 , 不是乱 , 而是提高效率
证明cpu是乱序的, 使用java代码证明 了解一下
public class Test02 {
private static int x=0,y=0;
private static int a=0,b=0;
public static void main(String[] args) throws InterruptedException {
int i=0;
for (; ; ) {
i++;
x=0;y=0;
a=0;b=0;
Thread one=new Thread(new Runnable() {
@Override
public void run() {
//由于线程one先启动,下面这句话让它等一等线程two. 可以根据自己的电脑进行设置
//shortWait(1000);
a=1;
x=b;
}
});
Thread other=new Thread(new Runnable() {
@Override
public void run() {
b=1;
y=a;
}
});
one.start();other.start();
one.join();other.join();
String result = "第" + i + "次(" + x + "," + y + ")";
if (x == 0 && y == 0) {
System.err.println(result);
break;
}else {
//不做处理
/*
* 情况分析 :
* 1, 正常的情况下会出现 x=0,y=1
* y=1,x=0;
* x=1,y=1;
* */
}
}
}
}
运行结果
半初始化状态的概念:
Object o=new Object()有那几个执行步骤?
汇编结果展示:
0 new #2 <java/lang/Object>
3 dup
4 invokespecial #1 <java/lang/Object.<init>>
7 astore_1
8 return
半初始化状态:
- 对象在new 出来是默认值是null ,经过init方法后才会变成我们想要的
3.单例讲解
单例的创建方式
-
/* * 饿汉式 * 类加载到内存后,就实例化一个单例,jvm保证线程安全 * 简单实用,推荐使用 * 唯一缺点: 不管用到与否,类装载时就会完成实例化 * Class.forName("") * */ public class Demo01 { private static final Demo01 INSTANCE = new Demo01(); //构造方法私有化,不允许别人new对象 private Demo01() { } //无论调用多少次getInstance方法,拿到的都是唯一的一个INSTANCE对象 public static Demo01 getInstance() { return INSTANCE; } public void m() { System.out.println("m"); } public static void main(String[] args) { Demo01 demo01 = Demo01.getInstance(); Demo01 demo02 = Demo01.getInstance(); System.out.println(demo01==demo02);//true } }
-
/* * 优化了,无论用不用都要初始化的问题 * 产生了问题: * 虽然达到了按需初始化的目的,但却带来线程不安全的问题 * */ public class Mgr03 { private static Mgr03 INSTANCE; private Mgr03(){} public static Mgr03 getInstance() { if (INSTANCE == null) { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } INSTANCE = new Mgr03(); } return INSTANCE; } public void m() { System.out.println("m"); } public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread(()-> System.out.println(Mgr03.getInstance().hashCode())).start(); } } //结果 1703607146 877465552 1703607146 2062736005 }
-
/* * 虽然达到了按需初始化的目的,但却带来线程不安全的问题 * 解决方法,加锁 * 产生了问题: 锁的粒度太粗,锁住的代码过多,效率变得很低 * */ public class Mgr04 { private static Mgr04 INSTANCE; private Mgr04(){} public synchronized static Mgr04 getInstance() { //业务逻辑,此处省略了一万行 if (INSTANCE == null) { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } INSTANCE = new Mgr04(); } return INSTANCE; } public void m() { System.out.println("m"); } public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread(()-> System.out.println(Mgr04.getInstance().hashCode())).start(); } } }
-
/* * 感觉上解决了锁的粒度问题,实际上并没有 * */ public class Mgr05 { private static Mgr05 INSTANCE; private Mgr05(){} public static Mgr05 getInstance() { if (INSTANCE == null) { //第一步判断会出现线程安全问题 synchronized (Mgr05.class){ try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } INSTANCE = new Mgr05(); } } return INSTANCE; } public void m() { System.out.println("m"); } public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread(()-> System.out.println(Mgr05.getInstance().hashCode())).start(); } } }
-
/* * 双重检查解决方案DCL //此手法用户众多开源软件的项目中 ==重点掌握== * 问题的来源 DCL单例到底要不要加volatile, * */ public class Mgr06 { //private static Mgr06 INSTANCE; private volatile static Mgr06 INSTANCE; private Mgr06(){} public static Mgr06 getInstance() { if (INSTANCE == null) { synchronized (Mgr06.class){ if (INSTANCE==null){ //Double Check Lock try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } INSTANCE = new Mgr06(); } } } return INSTANCE; } public void m() { System.out.println("m"); } public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread(()-> System.out.println(Mgr06.getInstance().hashCode())).start(); } } }
美团面试题: DCL单例要不要加volatile?答案: 必须加
- 因为程序是可以乱序执行的
- new 一个对象是存在半初始化状态的
存在这种可能( 该可能概率就是在大海里找到了我标记的一粒沙子 )
第一个线程进来才执行了半初始化指令
后面发生了指令重排序(cpu乱来了) 执行了astore_1指令( 建立了引用连接 ) , 使得 t 指向了 m=0 然而我的init还没有执行
第二个线程进来了判断了第一个是否==null 发现不是,它是半初始化的了
第二个线程直接没有进入synchronized代码块 , 最后拿着半初始化状态的对象跑了
图解:
结论
volatile修饰的变量在内存空间中 对它执行的指令不可以乱序
防止了其他线程拿到一个半初始化的对象
引发的问题
volatile在底层是怎么防止指令重排序的呢?
这就是内存屏障
jvm中的内存屏障:
屏障两边的指令不可以重拍! 保证有序!
底层实现追溯到汇编指令就是
Lock
和addl
指令