volatile低配版syn
序
since: 2019年8月25日 22:43
auth: Hadi
参考:
http://ifeve.com/jvm-memory-reordering/
JMM java内存模型
Java Memory Model Java内存模型
本身是一种抽象的概念,并不真实存在。他描述的是一组规范。通过规范定义了程序中各个变量(包括实例字段、静态字段和构成数组对象的元素)的访问方式。
JMM关于同步的规定:
- 线程解锁前,必须把共享变量的值,刷新回主内存。
- 线程加锁前,必须读取主内存的最新值到自己的工作内存
- 加锁解锁是同一把锁。
JVM相关规定:
由于JVM运行程序的实体是线程,而每个线程创建时,JVM都会为其创建一个工作内存(栈空间)。每个线程都有自己的栈空间,存放线程的私有数据。
而Java内存模型中规定所有变量都存储在主内存。 主内存是共享内存区域,所有的线程都可以访问。
但线程对变量的操作(读取赋值等)必须在工作内存中进行,就先要将变量从主内存中拷贝到自己的工作内存空间中,
然后对变量进行操作,操作完成后再回写到主内存中。不能直接操作主内存中的变量,各个线程的工作内存中存储着内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通讯必须通过主内存来完成,如下图所示:
多线程并发性质
可见性:
当多个线程操作同一个内存对象值的时候,可能大家一起拷贝了,然后有其中某些线程进行了更改。当有线程进行更改的时候,其他线程如果知道该值被更改,称之为 可见性。
原子性:
一个操作是不可以中断的,要么全部执行成功,要么全部执行失败,不许部分成功。一般来说我们在进行某个操作的时候,CPU会将其分解成多步,依次*执行。
有序性:
在java中的表现为,在当前线程是有序执行的,但是在多个线程进行操作的时候,是无序的。上述说CPU将一个指令分解为多步,依次执行不完全正确,因为多步按照不同进行了同步执行。
Volatile 提供轻量级的同步机制
Volatile 保证可见性、不保证原子性、禁止指令重排了,实现有序性。
为什么会有指令重排,点击跳转
比如 新建了一个class 中有个属性 number = 0、方法 add(this.number = 1);当 主程序 开启一个线程,更改程序 主程序一直判断number ==0 但没有可见性,Main程序一直傻傻while,代码如下所示:
以上代码是无可见性的,while会一直循环。现在对number变量上添加volatile,使其增加可见性:
volatile不保证原子性
-
原子性是什么?
不可分割、完整性。
就是做某个具体业务的时候,中间不可以被加塞或者被分割。需要同时成功/失败 -
volatile不保证原子性
可以做直接add添加 synchronized 每次只能一个线程使用这个。
但使用synchronized 式杀鸡用牛刀 -
为什么?n++ 会被拆分为几个CPU级别指令:
执行getField拿到内存中的值
执行iconst_1 拿到1的基准值
执行iadd对这两个值进行add操作的操作
执行putField将累加的值写入内存中。
如果这个在putfield 的时候被挂起(可能是其它线程获得了切片时间),唤醒的时候直接就putfield了。造成写覆盖,当前进行的putfield又获得CPU执行时,覆盖了另外线程写入的值。
如何解决原子性?
- Synchronized
使用Sync可以对变量等进行锁的控制。 - Java.utile.concurrent.atomic 中有 原子性包装的整形类 AtomicInteger 不需要volatile
Atomic类本身就是实现了对原子性的操作,对iconst_1 和iadd 进行了底层系统级别封装,保证了这几步的原子操作。
禁止指令重排实现有序性
代码执行
指令重排
计算机在执行程序的时候,为了提高性能,编译器和处理器常常会对指令做重排,一般分为以下三种
- 编译器优化
- 指令并行
- 内存系统
单线程环境里确保程序执行的结果和代码顺序执行的结果是一致的。但是在多线程中处理器就会造成指令重排,将CPU利用率最大化。在进行重排序时必须要考虑指令之间的数据依赖性(先有你爹,才能有你)。
多线程中,线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。(对于CPU的指令重排优化几乎就是:先把会做的先做了。)
示例
对于指令重排来说,只要符合数据依赖性,基本都能打乱。如果这个时候我们创建了两个线程同时跑更改的代码,那么结果会有哪些?
// 两个线程对以下变量进行同时操作
Int a, b, x, y = 0;
// 线程 1
x = a;
a = 2;
// 线程 2
y = b;
b = 1;
结果:
a,b,x,y
2,1,0,1
2,1,2,1
2,1,0,0
2,1,2,0
volatile 实现禁止指令重排优化
volatile通过实现禁止指令重排,从而避免多线程环境下程序出现乱序执行的现象。
内存屏障 Memory Barrier 又称内存栅栏,是一个CPU指令,它的作用有两个:
1. 保证特定操作的执行顺序
2. 保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)
由于编译器和处理器都能执行指令重排优化。如果在指令集间插入一条Memory Barrier 则会告诉编译器和CPU,不管什么指令都不能和Memory Barrier指令重排序,也就是说 通过插入内存屏障禁止在内存屏障前后的指令执行重排优化。
内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因为任何CPU上的线程都能读取到这些数据的最新样本。
对Volatile变量进行 写 操作时,在写操作后加入一条store屏障指令,将在工作内存中的共享变量值刷新回主内存.
对Volatile变量进行 读 操作时,毁在读操作前加入一条load屏障指令,从主内存中读取共享变量.
数据安全性获得保证
-
工作内存 和 主内存同步延迟现象导致的可见性问题,可以使用synchronized 或 volatile 关键字解决,他们都可以使一个线程 修改后的变量立即对其他线程可见。
-
对指令重排导致的可见性问题和有序性问题,可以使用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化
你在哪些地方用到过Volatile
单例模式DCL代码
DCL双端检锁 不一定线程安全,原因是有指令重排序的存在,必须加入volatile可以禁止指令重排
原因: 某一个线程执行到第一次检测,读取到instanc不为null时,instance的应用对象可能没有完成初始化。
Instance = new S(); 可分为三步:
Memory = allocate(); 分配对象内存空间
Instance(memory); 初始化对象
Instance = memory; 设置instance指向刚分配的内存地址,此时instance != null;
以上 第二步骤和第三步骤 没有数据依赖关系,可能被重排优化。
所以instance 可能!=null 但对象还没有初始化完成。
导致,不为null 但没有值! -> 线程安全。
代理模式volatile分析
所以添加volatile
Private static volatile 单例对象 instance = null;