Java中volatile关键字

1、简介java

volatile是Java提供的一种轻量级的同步机制。Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量,相比于synchronized(synchronized一般称为重量级锁),volatile更轻量级,由于它不会引发线程上下文的切换和调度。可是volatile 变量的同步性较差(有时它更简单而且开销更低),并且其使用也更容易出错。c++

2、并发编程的3个基本概念
(1)原子性编程

定义: 即一个操做或者多个操做 要么所有执行而且执行的过程不会被任何因素打断,要么就都不执行。缓存

原子性是拒绝多线程操做的,不管是多核仍是单核,具备原子性的量,同一时刻只能有一个线程来对它进行操做。简而言之,在整个操做过程当中不会被线程调度器中断的操做,均可认为是原子性。例如 a=1是原子性操做,可是a++和a +=1就不是原子性操做。Java中的原子性操做包括:安全

a. 基本类型的读取和赋值操做,且赋值必须是数字赋值给变量,变量之间的相互赋值不是原子性操做。markdown

b.全部引用reference的赋值操做多线程

c.java.concurrent.Atomic.* 包中全部类的一切操做并发

(2)可见性ide

定义:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其余线程可以当即看获得修改的值。性能

在多线程环境下,一个线程对共享变量的操做对其余线程是不可见的。Java提供了volatile来保证可见性,当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会当即被更新到主内存中,其余线程读取共享变量时,会直接从主内存中读取。固然,synchronize和Lock均可以保证可见性。synchronized和Lock能保证同一时刻只有一个线程获取锁而后执行同步代码,而且在释放锁以前会将对变量的修改刷新到主存当中。所以能够保证可见性。

(3)有序性

定义:即程序执行的顺序按照代码的前后顺序执行。

Java内存模型中的有序性能够总结为:若是在本线程内观察,全部操做都是有序的;若是在一个线程中观察另外一个线程,全部操做都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工做内存主主内存同步延迟”现象。

在Java内存模型中,为了效率是容许编译器和处理器对指令进行重排序,固然重排序不会影响单线程的运行结果,可是对多线程会有影响。Java提供volatile来保证必定的有序性。最著名的例子就是单例模式里面的DCL(双重检查锁)。另外,能够经过synchronized和Lock来保证有序性,synchronized和Lock保证每一个时刻是有一个线程执行同步代码,至关因而让线程顺序执行同步代码,天然就保证了有序性。

3、锁的互斥和可见性
锁提供了两种主要特性:互斥(mutual exclusion) 和可见性(visibility)。

(1)互斥即一次只容许一个线程持有某个特定的锁,一次就只有一个线程可以使用该共享数据。

(2)可见性要更加复杂一些,它必须确保释放锁以前对共享数据作出的更改对于随后得到该锁的另外一个线程是可见的。也即当一条线程修改了共享变量的值,新值对于其余线程来讲是能够当即得知的。若是没有同步机制提供的这种可见性保证,线程看到的共享变量多是修改前的值或不一致的值,这将引起许多严重问题。要使 volatile 变量提供理想的线程安全,必须同时知足下面两个条件:

a.对变量的写操做不依赖于当前值。

b.该变量没有包含在具备其余变量的不变式中。

实际上,这些条件代表,能够被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。事实上就是保证操做是原子性操做,才能保证使用volatile关键字的程序在并发时可以正确执行。

4、Java的内存模型JMM以及共享变量的可见性
JMM决定一个线程对共享变量的写入什么时候对另外一个线程可见,JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,每一个线程都有一个私有的本地内存(Local Memory),本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的全部操做都必须在工做内存中进行,而不能直接读写主内存中的变量。
Java中volatile关键字
对于普通的共享变量来说,线程A将其修改成某个值发生在线程A的本地内存中,此时还未同步到主内存中去;而线程B已经缓存了该变量的旧值,因此就致使了共享变量值的不一致。解决这种共享变量在多线程模型中的不可见性问题,较粗暴的方式天然就是加锁,可是此处使用synchronized或者Lock这些方式过重量级了,比较合理的方式其实就是volatile。

须要注意的是,JMM是个抽象的内存模型,因此所谓的本地内存,主内存都是抽象概念,并不必定就真实的对应cpu缓存和物理内存

5、volatile变量的特性
(1)保证可见性,不保证原子性

a.当写一个volatile变量时,JMM会把该线程本地内存中的变量强制刷新到主内存中去;

b.这个写会操做会致使其余线程中的缓存无效。

(2)禁止指令重排

重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。重排序须要遵照必定规则:

  a.重排序操做不会对存在数据依赖关系的操做进行重排序。

  好比:a=1;b=a; 这个指令序列,因为第二个操做依赖于第一个操做,因此在编译时和处理器运

行时这两个操做不会被重排序。

b.重排序是为了优化性能,可是无论怎么重排序,单线程下程序的执行结果不能被改变

  好比:a=1;b=2;c=a+b这三个操做,第一步(a=1)和第二步(b=2)因为不存在数据依赖关系, 因此可能会发

生重排序,可是c=a+b这个操做是不会被重排序的,由于须要保证最终的结果必定是c=a+b=3。

重排序在单线程下必定能保证结果的正确性,可是在多线程环境下,可能发生重排序,影响结果,下例中的1和2因为不存在数据依赖关系,则有可能会被重排序,先执行status=true再执行a=2。而此时线程B会顺利到达4处,而线程A中a=2这个操做还未被执行,因此b=a+1的结果也有可能依然等于2。
Java中volatile关键字
使用volatile关键字修饰共享变量即可以禁止这种重排序。若用volatile修饰共享变量,在编译时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序,volatile禁止指令重排序也有一些规则:

a.当程序执行到volatile变量的读操做或者写操做时,在其前面的操做的更改确定所有已经进行,且结果已经对后

面的操做可见;在其后面的操做确定尚未进行;

b.在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放

到其前面执行。

即执行到volatile变量时,其前面的全部语句都执行完,后面全部语句都未执行。且前面语句的结果对volatile变

量及其后面语句可见。

6、volatile不适用的场景
(1)volatile不适合复合操做

例如,inc++不是一个原子性操做,能够由读取、加、赋值3步组成,因此结果并不能达到30000。.
Java中volatile关键字
(2)解决方法

1.采用synchronized
Java中volatile关键字
2.采用Lock
Java中volatile关键字
3.采用java并发包中的原子操做类,原子操做类是经过CAS循环的方式来保证其原子性的
Java中volatile关键字
7、volatile原理
volatile能够保证线程可见性且提供了必定的有序性,可是没法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上至关于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

I. 它确保指令重排序时不会把其后面的指令排到内存屏障以前的位置,也不会把前面的指令排到内

存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操做已经所有完成;

II. 它会强制将对缓存的修改操做当即写入主存;

III. 若是是写操做,它会致使其余CPU中对应的缓存行无效。

8、单例模式的双重锁为何要加volatile
Java中volatile关键字
须要volatile关键字的缘由是,在并发状况下,若是没有volatile关键字,在第5行会出现问题。instance = new TestInstance();能够分解为3行伪代码

a.memory = allocate() //分配内存

b. ctorInstanc(memory) //初始化对象

c. instance = memory //设置instance指向刚分配的地址

上面的代码在编译运行时,可能会出现重排序从a-b-c排序为a-c-b。在多线程的状况下会出现如下问题。当线程A在执行第5行代码时,B线程进来执行到第2行代码。假设此时A执行的过程当中发生了指令重排序,即先执行了a和c,没有执行b。那么因为A线程执行了c致使instance指向了一段地址,因此B线程判断instance不为null,会直接跳到第6行并返回一个未初始化的对象。

原文连接:https://blog.csdn.net/u012723673/article/details/80682208

相关文章
相关标签/搜索