赞
踩
本文通过学习:周阳老师-尚硅谷Java大厂面试题第二季 总结的volatile相关的笔记
volatile是Java虚拟机提供的轻量级的同步机制,三大特性为:
保证可见性、不保证原子性、禁止指令重排
- import java.util.concurrent.TimeUnit;
-
- class MyData {//主物理内存
- volatile int number = 0;
- public void addTo60() {
- this.number = 60;
- }
- }
-
- public class VolatileDemo {
- public static void main(String args []) {
- MyData myData = new MyData();
- new Thread(() -> {
- System.out.println(Thread.currentThread().getName() + "\t come in");
-
- try {
- TimeUnit.SECONDS.sleep(3);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
-
- myData.addTo60();
- System.out.println(Thread.currentThread().getName()
- + "\t update number value:" + myData.number);
-
- }, "AAA").start();
-
- while(myData.number == 0) {
- }
- //说明AAA线程在睡眠3秒后,更新的number的值,重新写入到主内存,并被main线程感知到了
- System.out.println(Thread.currentThread().getName() + "\t mission is over");
- }
- }
- /**
- * AAA come in
- * AAA update number value:60
- * main mission is over //若number=0没被volatile修饰,则这句不打印
- */
- import java.util.concurrent.TimeUnit;
-
- class MyData {
- volatile int number = 0;
- public void addPlusPlus() {
- number ++;
- }
- }
-
- public class VolatileDemo {
- public static void main(String args []) {
- MyData myData = new MyData();
- for (int i = 0; i < 20; i++) {
- new Thread(() -> {
- for (int j = 0; j < 1000; j++) {
- myData.addPlusPlus();
- }
- }, String.valueOf(i)).start();
- }
-
- // 需要等待上面20个线程都计算完成后,在用main线程取得最终的结果值
- // 这里判断线程数是否大于2,为什么是2?因为默认是有两个线程的,一个main线程,一个gc线程
- while(Thread.activeCount() > 2) {
- Thread.yield();//yield表示不执行
- }
-
- // 最终输出的值应该=20*1000=20000
- System.out.println(Thread.currentThread().getName()
- + "\t finally number value: " + myData.number);//19504
- }
- }
线程1和2同时修改各自工作空间中的内容,因为可见性,需要重写入内存,但
线程1在写入的时候,线程2也同时写入,导致线程1的写入操作被挂起,导致
线程2先写,线程1后写,线程1的值覆盖了线程2的值,因此数据丢失。
n++这条命令,被拆分成了3个指令:
-getfield 从主内存拿到原始n
-iadd 进行加1操作
-putfileld 把累加后的值写回主内存
假如三个线程同时通过getfield命令,拿到主存中的n值,然后三个线程,各自在自己的工作内存中进行加1操作,但他们并发进行 iadd 命令的时候,因为只能一个进行写,所以其它操作会被挂起,假设1线程,先进行了写操作,在写完后,volatile的可见性,应该需要告诉其它两个线程,主内存的值已经被修改了,但是因为太快了,其它两个线程,陆续执行 iadd命令,进行写入操作,这就造成了其他线程没有接受到主内存n的改变,从而覆盖了原来的值,出现写丢失,这样也就让最终的结果少于200
public synchronized void addPlusPlus() { number ++; } |
AtomicInteger atomicInteger = new AtomicInteger(); public void addAtomic() { atomicInteger.getAndIncrement(); } |
指令重排的代码示例
public class ResortSeqDemo { int a= 0; boolean flag = false; public void method01() { a = 1; flag = true; } public void method02() { if(flag) { a = a + 5; System.out.println("reValue:" + a); } } } | 【顺序执行】 a=1 flag=true a=a+5 顺序执行,打印reValue:6 【指令重排】 flag=true a=a+5 打印reValue:5 a=1 |
| 方法1. synchronized 方法2. 禁用指令重排 + DCL双端检锁 DCL = Double Check Lock 双端检锁机制 |
- public class SingletonDemo {
- private static volatile SingletonDemo instance = null;
- private SingletonDemo() {
- System.out.println(Thread.currentThread().getName() + "\t 我是构造方法SingletonDemo");
- }
-
- public static SingletonDemo getInstance() {
- if(instance == null) {
- synchronized (SingletonDemo.class) {
- if(instance == null) {
- instance = new SingletonDemo();
- }
- }
- }
- return instance;
- }
-
- public static void main(String[] args) {
- for (int i = 0; i < 10; i++) {
- new Thread(() -> {
- SingletonDemo.getInstance();
- }, String.valueOf(i)).start();
- }
- }
- }
- /*
- * 0 我是构造方法SingletonDemo
- */
原因是在某一个线程执行到第一次检测的时候,读取到 instance 不为null,instance的引用对象可能没有完成实例化。因为 instance = new SingletonDemo();可以分为以下三步进行完成:
memory = allocate(); // 1、分配对象内存空间
instance(memory); // 2、初始化对象
instance = memory; // 3、设置instance指向刚刚分配的内存地址,此时instance != null
但是我们通过上面的三个步骤,能够发现,步骤2 和 步骤3之间不存在 数据依赖关系,而且无论重排前 还是重排后,程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。
memory = allocate(); // 1、分配对象内存空间
instance = memory; // 3、设置instance指向刚刚分配的内存地址,此时instance != null,但是对象还没有初始化完成
instance(memory); // 2、初始化对象
这样就会造成什么问题呢?
也就是当我们执行到重排后的步骤2,试图获取instance的时候,会得到null,因为对象的初始化还没有完成,而是在重排后的步骤3才完成,因此执行单例模式的代码时候,就会重新在创建一个instance实例
指令重排只会保证串行语义的执行一致性(单线程),但并不会关系多线程间的语义一致性
所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,这就造成了线程安全的问题, 因此需要引入volatile,来保证出现指令重排的问题,从而保证单例模式的线程安全性。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。