当前位置:   article > 正文

Java Volatile的三大特性_java volatile特点

java volatile特点

本文通过学习:周阳老师-尚硅谷Java大厂面试题第二季 总结的volatile相关的笔记

volatile是Java虚拟机提供的轻量级的同步机制,三大特性为:

保证可见性、不保证原子性、禁止指令重排

一、保证可见性

  1. import java.util.concurrent.TimeUnit;
  2. class MyData {//主物理内存
  3. volatile int number = 0;
  4. public void addTo60() {
  5. this.number = 60;
  6. }
  7. }
  8. public class VolatileDemo {
  9. public static void main(String args []) {
  10. MyData myData = new MyData();
  11. new Thread(() -> {
  12. System.out.println(Thread.currentThread().getName() + "\t come in");
  13. try {
  14. TimeUnit.SECONDS.sleep(3);
  15. } catch (InterruptedException e) {
  16. e.printStackTrace();
  17. }
  18. myData.addTo60();
  19. System.out.println(Thread.currentThread().getName()
  20. + "\t update number value:" + myData.number);
  21. }, "AAA").start();
  22. while(myData.number == 0) {
  23. }
  24. //说明AAA线程在睡眠3秒后,更新的number的值,重新写入到主内存,并被main线程感知到了
  25. System.out.println(Thread.currentThread().getName() + "\t mission is over");
  26. }
  27. }
  28. /**
  29. * AAA come in
  30. * AAA update number value:60
  31. * main mission is over //若number=0没被volatile修饰,则这句不打印
  32. */

二、不保证原子性

1、代码示例

  1. import java.util.concurrent.TimeUnit;
  2. class MyData {
  3. volatile int number = 0;
  4. public void addPlusPlus() {
  5. number ++;
  6. }
  7. }
  8. public class VolatileDemo {
  9. public static void main(String args []) {
  10. MyData myData = new MyData();
  11. for (int i = 0; i < 20; i++) {
  12. new Thread(() -> {
  13. for (int j = 0; j < 1000; j++) {
  14. myData.addPlusPlus();
  15. }
  16. }, String.valueOf(i)).start();
  17. }
  18. // 需要等待上面20个线程都计算完成后,在用main线程取得最终的结果值
  19. // 这里判断线程数是否大于2,为什么是2?因为默认是有两个线程的,一个main线程,一个gc线程
  20. while(Thread.activeCount() > 2) {
  21. Thread.yield();//yield表示不执行
  22. }
  23. // 最终输出的值应该=20*1000=20000
  24. System.out.println(Thread.currentThread().getName()
  25. + "\t finally number value: " + myData.number);//19504
  26. }
  27. }

2、数值丢失的原因?

线程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

3、解决办法(synchronized / AtomicInteger)

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 双端检锁机制

  1. public class SingletonDemo {
  2. private static volatile SingletonDemo instance = null;
  3. private SingletonDemo() {
  4. System.out.println(Thread.currentThread().getName() + "\t 我是构造方法SingletonDemo");
  5. }
  6. public static SingletonDemo getInstance() {
  7. if(instance == null) {
  8. synchronized (SingletonDemo.class) {
  9. if(instance == null) {
  10. instance = new SingletonDemo();
  11. }
  12. }
  13. }
  14. return instance;
  15. }
  16. public static void main(String[] args) {
  17. for (int i = 0; i < 10; i++) {
  18. new Thread(() -> {
  19. SingletonDemo.getInstance();
  20. }, String.valueOf(i)).start();
  21. }
  22. }
  23. }
  24. /*
  25. * 0 我是构造方法SingletonDemo
  26. */

原因是在某一个线程执行到第一次检测的时候,读取到 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,来保证出现指令重排的问题,从而保证单例模式的线程安全性。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小丑西瓜9/article/detail/456997
推荐阅读
相关标签
  

闽ICP备14008679号