JUC(三) java内存模型,volatile关键字

java 内存模型

整体流程

JMM规定了所有的变量都存储在主内存(Main Memory)中。每个线程还有自己的工作内存(Working Memory),
线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,
而不能直接读写主内存中的变量(volatile变量仍然有工作内存的拷贝,但是由于它特殊的操作顺序性规定,所以看起来如同直接在主内存中读写访问一般)。
不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成。

可见性 原子性 有序性

线程安全取决于是否有多个线程访问(说句废话),避免线程安全问题主要从原子性,可见性,有序性着手

1.原子性 同生共死 – 要么全部执行要不全部不执行不可中断!

举例:

 x = 10  原子性操作 直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中
 y = x  非原子操作 实际上包含2个操作,它先要去读取x的值,再将x的值写入工作内存,
 虽然读取x的值以及 将x的值写入工作内存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。
 x++  非原子操作 x++、x = x+1包括3个操作:读取x的值,进行加1操作,写入新的值。
 x = x + 1 非原子操作

注意: 在32位平台下,对64位数据的读取和赋值是需要通过两个操作来完成的,不能保证其原子性,这就导致了long、double类型的变量在32位虚拟机中是非原子操作

2.可见性

可见性就是对一个变量进行修改,其他的线程能够立即看到修改的值,JMM利用主内存。工作内存修改后刷新到主内存,别的线程调用时会把主内存的最新得值刷新到工作内存
这种方式是依赖主内存来实现可见性

举例 :

    int i = 1;//主内存
    //线程1执行的代码
    i = 10;
    //线程2执行的代码
    j = i;

主内存变量A原始值为1,线程1从主内存取出变量A,修改A的值为2,在线程1未将变量A写回主内存的时候,线程2拿到变量A的值仍然为1 造成可见性问题

Java提供了volatile关键字来保证可见性。当对非volatile变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中,非volatile变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。而声明变量是volatile的,会保证修改的值会立即被更新到主存,当有其他线程需要读取时,
它会跳过CPU cache这一步去内存中读取新值。volatile只确保了可见性,并不能确保原子性

3.有序性

指令重排序:

即程序执行的顺序按照代码的先后顺序执行。为了提高性能,编译器和处理器常常会对指令做重排序。CPU虽然并不保证完全按照代码顺序执行,
但它会保证程序最终的执行结果和代码顺序执行时的结果一致。
重排序过程不会影响到单线程程序的执行,但会影响到多线程并发执行的正确性。

//线程1:
context = loadContext();   //语句1
inited = true;             //语句2
 
//线程2:
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);


代码中,由于语句1和语句2没有数据依赖性,可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,
那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。

可以通过volatile关键字来保证一定的“有序性”,volatile关键字本身就包含了禁止指令重排序的语义,volatile前的代码还会在voaltile前,
其后的代码还会在其后。另外可以通过synchronized和Lock来保证有序性,synchronized和Lock保证每个时刻是有一个线程执行同步代码,
相当于是让线程顺序执行同步代码,自然就保证了有序性,synchronized标记的变量可以被编译器优化。

也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。


volatile 变量:轻量级多线程同步机制,不会引起上下文切换和线程调度。仅提供内存可见性保证,不提供原子性。
CAS 原子指令:轻量级多线程同步机制,不会引起上下文切换和线程调度。它同时提供内存可见性和原子化更新保证。
内部锁和显式锁:重量级多线程同步机制,可能会引起上下文切换和线程调度,它同时提供内存可见性和原子性。


https://juejin.im/entry/59ba2a22f265da065d2b5789 引用此文 -- 更加精细

valatile 解析

IDProblemArticle
000volatile是什么解决思路
001JMM内存模型值之可见性解决思路
002可见性验证说明解决思路
003volatile不保证原子性解决思路
004volatile指令重构排序解决思路
005单例模式在多线程下可能存在的问题解决思路
006单例模式volatile分析解决思路

=======================================================================================================================

            synchronized                              | volatile
         
   比较: 阻塞式同步,在线程竞争激烈的情况下会升级为重量级锁 | java虚拟机提供的最轻量级的同步机制
   
   各个线程会将共享变量从主内存中拷贝到工作内存,然后执行引擎会基于工作内存中的数据进行操作处理。
   线程在工作内存进行操作后何时会写到主内存中?这个时机对普通变量是没有规定的,而针对volatile修饰的变量给java虚拟机特殊的约定,
   线程对volatile变量的修改会立刻被其他线程所感知,即不会出现数据脏读的现象,从而保证数据的“可见性”。

被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象

实现原理:

在这里插入图片描述

happens-before 6大原则:

在这里插入图片描述

对于这个valatile 的规则 写先发生于读 保证了可见性 /VolatileExample/VolatileExample /VolatileExample/Volatile2

i++问题解决方法:加锁!/VolatileExample/AccountingSync

volatile
   volatile是什么
   
   volatie 可以保证不同线程之间的交互可见性   
   1.轻量级同步锁(竞争线程不会阻塞)
   2.保证可见性
   3.不保证原子性
   
   
   JMM内存模型值之可见性
   
  JMM关于同步的规定 
  
  线程解锁前 必须把共享变量的值栓回到主内存
  线程加锁前,必须把主内存最新的值更新到自己的内存中
  加锁解锁是同一把锁
  
  
  由于JVM的运行程序的实体是线程 而每个线程创建时JVM都会为其创建一个自己的工作内存,工作内存是每个线程的私有数据区域
  而java内存模型中规定所有的变量都储存在主内存中,主内存是共享内存区域所有的线程都可以访问,但是线程对变量的操作读值赋值等必须在工作内存中进行
  ,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回到主内存,不能直接操作主内存中的变量,各个线程中的
  工作内存存储着主内存的变量副本拷贝,因此不同线程无法访问对方的工作内存,线程间的通信传值必须通过主内存来完成!

主内存是共享内存区域所有的线程都可以访问,但是线程对变量的操作读值赋值等必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间, 然后对变量进行操作,操作完成后再将变量写回到主内存,不能直接操作主内存中的变量,各个线程中的工作内存存储着主内存的变量副本拷贝

整体流程

volatile不保证原子性

VolatileDemo1

运行结果表明,volatile没有保证原子性,出现丢失写值的情况(值覆盖)

VolatileDemo2

atomicInteger 解决原子性问题

volatile 禁止指令重排序

整体流程

volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象

先了解一个概念,内存屏障(Memory Barrier) 又称内存栅栏,是一个CPU指令,它的作用有两个:

一是保证特定操作的执行顺序,

二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)

由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,
不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。
内存屏障另外一个作用就是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。

[外链图片转存失败(img-Pt5fQ5tq-1564015404527)(https://raw.githubusercontent.com/qiurunze123/imageall/master/volatile4.png)]

工作内存与主内存同步延迟现象导致的可见性问题
可以使用synchronized或volatile关键字解决,他们都可以是一个线程修改后的变量立刻对其他线程可见
对于指令重排导致的可见性问题和有序性问题
可以利用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化
单例模式在多线程下可能存在的问题以及分析
上述代码采用了DCL(Double Check Lock)的双端检锁机制,但是还是不能保证线程安全,原因是由指令重排序的存在,加入volatile可以禁止指令重排

  原因在于某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化。

instance = new SingletonDemo();可能会分为以下三步(伪代码):

memory = allocate();//分配对象内存空间
	instance(memory); //初始化对象
	instance = memory;//设置instance执行刚分配的内存地址,此时instance != null
步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的

memory = allocate();//分配对象内存空间
	instance = memory;//设置instance执行刚分配的内存地址,此时instance != null,但是对象还没有完成初始化
	instance(memory); //初始化对象
但是指令重排只会保证串行语义的执行一致性(单线程),但并不会关心多线程间的语义一致性、

所以当一条线程访问instance不为null时,由于instance实例未必已完成初始化,也就造成了线程安全问题。

所以在  private static volatile SingletonDemo instance =null; 加volatile
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值