volatile低配版syn,实现可见性和有序性

Posted 青冬

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了volatile低配版syn,实现可见性和有序性相关的知识,希望对你有一定的参考价值。

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;

回顾

谈谈JMM模型

可见、原子、有序分别是什么

在Automic类中的是实现方式,保证以上什么特性

volatile实现了什么

CPU工作原理

以上是关于volatile低配版syn,实现可见性和有序性的主要内容,如果未能解决你的问题,请参考以下文章

Java多线程安全可见性和有序性之Volatile

并发之volatile关键字

voliate怎么保证可见性

java并发系列-----Java并发:volatile关键字解析

4个点说清楚Java中synchronized和volatile的区别

实现一个基于 IConfiguration 的低配版 FeatureFlag