当前位置: 首页 > news >正文

Java Volatile的三大特性

本文通过学习:周阳老师-尚硅谷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修饰,则这句不打印*/

二、不保证原子性

1、代码示例

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=20000System.out.println(Thread.currentThread().getName() + "\t finally number value: " + myData.number);//19504}
}

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

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,来保证出现指令重排的问题,从而保证单例模式的线程安全性。

http://www.lryc.cn/news/30776.html

相关文章:

  • Android Compose——一个简单的Bilibili APP
  • 二叉树的最近公共祖先【Java实现】
  • 关闭应用程序遥测,禁止Windows收集用户信息
  • 【备战面试】每日10道面试题打卡-Day4
  • 热乎的面经——初出茅庐
  • 数据库中各种锁汇总
  • p76 - Python 开发-内外网收集 Socket子域名DNS
  • QCC51XX--eFush Key加密
  • nginx http模块
  • 守护进程 || 精灵进程
  • Zookeeper3.5.7版本——客户端命令行操作(znode 节点数据信息)
  • 如何写好单测
  • CDH-6.3.2内置spark-2.4.0的BUG
  • SpringCloud之ElasticSearch笔记
  • 数字图像学笔记 —— 17. 图像退化与复原(自适应滤波之「最小二乘方滤波」)
  • 2023-03-05:ffmpeg推送本地视频至lal流媒体服务器(以RTMP为例),请用go语言编写。
  • MathType7最新版免费数学公式编辑器
  • 一文带你入门angular(中)
  • 单例设计模式共享数据问题分析、解决(c++11)设计多线程。
  • Embedding-based Retrieval in Facebook Search
  • xmu 离散数学 卢杨班作业详解【8-12章】
  • Linux入门篇-权限管理
  • Linux(基于 Centos7) 常用操作
  • Math类详解与Random类、三种随机数生成方式(java)
  • Mac编译QT程序出现Undefined symbols for architecture x86_64
  • 蓝桥杯-李白打酒加强版
  • AtCoder Beginner Contest 292 (A - E) 记录第一场ABC
  • ubuntu安装使用putty
  • 【CS144】Lab5与Lab6总结
  • GDScript 导出变量 (Godot4.0)