volatile简介
每个线程访问堆中对象时,将堆中对象load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆变量值有任何关系,而是直接修改副本变量值。
修改完之后,自动把线程变量副本的值写到对象在堆变量中。这样堆中对象的值就产生了变化。这些操作不是原子性的。
使用volatile修饰变量,JVM只是保证从内存加载到线程工作内存中的值是最新的。因此,即使使用volatile还是会存在并发情况。
volatile static int a=0;
线程A和线程B同时执行:
a++;
此时线程A拿到a的最新值0,线程B也拿到最新值0;但是,A执行a++后,值为1,B也同样计算得到a=1,它们再同时写回到堆内存,使得最后a的值为1,并不为2.
volatile关键字是Java并发的最轻量级实现,本质上有两个功能:
在生成的汇编语句中加入LOCK关键字和内存屏障
作用就是保证每一次线程load和write两个操作,都会直接从主内存中进行读取和覆盖,而非普通变量从线程内的工作空间。
但它有一个很致命的缺点,导致它的使用范围不多,就是他只保证在读取和写入这两个过程是线程安全的。
如果我们对一个volatile修饰的变量进行多线程下的自增操作,还是会出现线程安全问题。根本原因在于volatile关键字无法对自增进行安全性修饰,因为自增分为三步,读取-》+1-》写入。中间多个线程同时执行+1操作,还是会出现线程安全性问题。)
作用
- 可见性
- 禁止指令重排,无原子性
synchronized和volatile区别
- volatile仅能使用在变量级别,synchronized可以在变量、方法、类级别
- volatile仅能实现变量修改的可见性,不能保证原子性
- volatile不会造成线程阻塞
- volatile不会被编译器优化
不要将volatile用在getAndOperate场合,仅仅set或者get的场景是适合volatile的
volatile没有原子性举例:AtomicInteger自增
例如你让一个volatile的integer自增(i++),其实要分成3步:1)读取volatile变量值到local; 2)增加变量的值;3)把local的值写回,让其它的线程可见。这3步的jvm指令为:
mov 0xc(%r10),%r8d ; Load
inc %r8d ; Increment
mov %r8d,0xc(%r10) ; Store
lock addl $0x0,(%rsp) ; StoreLoad Barrier
注意最后一步是内存屏障。
内存屏障(Memory Barrier)
内存屏障是一个CPU指令。基本上,它是这样一条指令:
- a) 确保一些特定操作执行的顺序;
- b) 影响一些数据的可见性(可能是某些指令执行后的结果)。
编译器和CPU可以在保证输出结果一样的情况下对指令重排序,使性能得到优化。插入一个内存屏障,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。内存屏障另一个作用是强制更新一次不同CPU的缓存。例如,一个写屏障会把这个屏障前写入的数据刷新到缓存,这样任何试图读取该数据的线程将得到最新值,而不用考虑到底是被哪个cpu核心或者哪颗CPU执行的。
内存屏障和volatile关系
如果你的字段是volatile,Java内存模型将在写操作后插入一个写屏障指令,在读操作前插入一个读屏障指令。
这意味着如果你对一个volatile字段进行写操作,你必须知道:
1、一旦你完成写入,任何访问这个字段的线程将会得到最新的值。
2、在你写入前,会保证所有之前发生的事已经发生,并且任何更新过的数据值也是可见的,因为内存屏障会把之前的写入值都刷新到缓存。
volatile为什么没有原子性?
从Load到store到内存屏障,一共4步,其中最后一步jvm让这个最新的变量的值在所有线程可见,也就是最后一步让所有的CPU内核都获得了最新的值,但中间的几步(从Load到Store)是不安全的,中间如果其他的CPU修改了值将会丢失。
网友评论