请谈谈你对volatile的理解

volatile是java虚拟机提供的轻量级的同步机制,三个特性:

  • 保证可见性
  • 不保证原子性
  • 禁止指令重排
  • 它会强制从公共栈中取得变量的值,而不是从线程私有数据栈中取得变量的值
  • 只能修饰变量
  • volatile不支持原子性:
    对于volatile修饰的变量,JVM虚拟机只是保证从主内存加载到线程工作内存的值是最新的。在加载后这个值可能被其他线程改了,这时线程工作内存的值已经加载,不会变化了,也就是私有内存和公共内存中的变量不同步
    原文链接:https://blog.csdn.net/sakuragio/article/details/98997672

保证可见性:

JMM你谈谈:
JMM是java内存模型,本身是一种抽象概念,并不真实存在。他描述的是一组规则或规范,通过这组规范定义了程序中各个变量的访问方式。

JMM关于同步的规定:

  1. 线程解锁前,必须把共享变量的值刷新回主内存
  2. 线程加锁前,必须读取主内存的最新值到自己的工作内存
  3. 加锁解锁是同一把锁

由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存,工作内存是每个线程的私有数据区域,而java内存模型规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读、赋值)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信必须通过主内存完成。

这种能够第一时间通知的机制就是JMM中的 可见性

不保证原子性:

原子性:某个线程在做某个具体业务时,中间不可以被加塞或者分割,要么同时成功,要么同时失败。

如何在不使用Synchronized的情况下保证原子性?
可以使用原子类 AtomicInteger 等

禁止指令重排 / 有序性:

例子:

public void mySort() {
	int x = 11; //1
	int y = 12; //2
	x = x + 5; //3
	y = x + x; //4
}
代码顺序:1234
经过指令重排后,可能顺序为:21341324
4不可能排第一,因为处理器在进行重排时要考虑 数据的依赖性
int a,b,x,y = 0;

线程1: x = a; b = 1; start()
线程2: y = b; a = 2; start()
//结果 x=0;y=0

因为abxy没有数据依赖性,指令重排后可能会 先a=,再b=,再x=,再y=,结果:x=2;y=1;

计算机在执行程序时,为了提高性能,编译器和处理器擦河南工厂会对指令做重排,一般分为:编译器优化的重排、指令并行的重排、内存系统的重排
 
单线程环境里面可以确保程序的最终执行结果和代码顺序执行的结果一致
 
处理器在进行重排时必须要考虑指令之间的 数据依赖性
 
多线程环境线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测

内存屏障:一个CPU指令,作用是 1保证特定操作的执行顺序,2保证这些变量的内存可见性(利用该特性实现volatile的内存可见性)

  • 通过插入内存屏障禁止在内存屏障前后的指令进行重排序优化
  • 强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读到这些数据的最新版本

在volatile变量在进行写操作是,会在写操作后加入一条store屏障指令,将工作内存的共享变量值刷新回到主内存
对volatile变量进行读操作的时候,会在读操作前加入一条load指令,从主内存中读取共享变量

你在哪些地方用到过volatile?
单例模式double check lock dcl 双端检索机制

private static Singleton instance;

private Singleton() {
}

public Singleton getInstance() {
	if(instance == null) {
		synchronized(Singleton.class) {
			if(instance == null) {
				instance = new Singleton();
			}
		}
	}
	return instance;
} volatile 

DCL不一定线程安全,因为有指令重排的存在。

原因在于某一个线程执行第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化:
insatnce = new Singleton(); 可以分为以下3步(伪代码):

  1. memory = allocate(); 分配对象内存空间
  2. insatnce(memory); 初始化对象
  3. instance = memory; 设置insatnce只想刚分配的内存地址,此时instance != null
     

因为2 3部不存在数据依赖关系,而且无论重排前还是重排后,程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的:

  1. memory = allocate(); 分配对象内存空间
  2. instance = memory; 设置insatnce只想刚分配的内存地址,此时instance != null,但是对象还没有初始化完成,这个时候其他线程来访问instance,判断instance不为null,直接返回instance,但因为instance还没有初始化完成,所以为空
  3. insatnce(memory); 初始化对象

加入volatile可以禁止指令重排

private static volatile Singleton instance;

你可能感兴趣的:(#,JUC)