感谢老鼠只爱大米的blog,干货满满*_*
一、简介:
volatile 是Java提供的一种轻量级的同步机制。java语言包含两种内在的同步机制:一种是代码块synchronized(和volatile关键字),volatile更轻量级,不会引起上下文的调度。但是volatile的同步性较差,更容易出错。
二、并发编程的3个基本概念
(1)原子性
定义:一个操作或者多个操作要么全部执行不会被任何的因素打扰,要么就都不执行
原子性是拒绝多线程操作的,不论是多核还是单核,具有原子性的变量,同一时间只能有一个线程对他操作。简单的说,在执行过程中不被其他线程中断操作打扰。例如:a=0.但是a++和a+=1就不是原子性操作。原子性操作包括:
a.基本类型的赋值和读取操作,且必须是数字赋值的操作,变量之间的赋值操作不是原子性操作。
b.java.concurrent.Atomic.*包中所有类的一切操作
(2)可见性
定义:多个线程访问一个变量时,一个线程修改这个变量,其他线程立即可以看到修改后的变量
在多线程中,一个线程对共享变量的操作对其他的线程是不可改变。当volatile修饰后,表示本地内存无效,当一个线程修改了共享变量后,立马将共享变量的值刷会主内存中,其他线程读取共享变量的值的时候,直接从主存中读取。当然,synchronized和Lock都可以保证可见性,synchronized和lock能保证同一时刻只有一个线程获取锁进行同步代码,并且在释放锁之前将改变的值刷到主内存。从而保证了原子的可见性。
(3)有序性
定义:程序执行按照代码的先后顺序执行
Java内存模型中的有序性可以总结为:在一个线程操作都是有序的,在一个线程观看另一个线程操作是无序的。前指:“线程的串行化语言”,后一句是指“指令重排”现象和工作内存主内存同步延迟现象。
java内存模型中,为了效率是允许编译器和处理器对指令进行重排顺序,当然重排序不会影响单线程的执行。但是会影响多线程。Java通过volatile来保证一定的有序性。一会儿会有一个单例模式进行讲解一下。
三、锁的互斥和可见性
锁提供两中特性:互斥和可见性
(1)互斥即一次只允许一个线程持有某个特定的锁,一次就只有一个线程能够使用该共享数据。
(2)可见性要更加复杂一些,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的。也即当一条线程修改了共享变量的值,新值对于其他线程来说是可以立即得知的。如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:
a.对变量的写操作不依赖于当前值。
b.该变量没有包含在具有其他变量的不变式中。
实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。事实上就是保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。
四、Java的内存模型JMM以及共享变量的可见性
JMM决定一个线程对共享变量的写入何时对另一个线程可见,JMM定义了线程和主内存的抽象关系:共享变量存储在主内存中,每一个线程都有一个本地内存,本地内存保存了被该线程使用到的主内存的副本的拷贝。线程变量的所有操作都必须在工作内存中执行,而不能直接读写主内存的变量。
对于普通的共享变量来讲,线程A将其修改为某个值发生在本地内存A中,此时还未同步到主内存中,而线程B已经缓存了旧值,就会引起数据不一致。解决方案:可以加锁synchronized或lock,在释放前将数据刷入主内存,但是太重量级,建议使用volatile。
五、volatile的特性
(1)保证可见性,不保证原子性
当写了一个volatile变量时,JMM会把该线程本地内存中的变量刷入主内存之中。
这个操作会将其它线程的缓存无效
(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
使用volatile关键字修饰共享变量可以阻止这个新的排序。使用volatile关键字修饰共享变量,在编译时,会在指令序列中插入内存屏障来阻止这个指令重排。volatile禁止指令重排的规则:
a、当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见
在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行
六、volatile不适用的场景
volatile不适用重复操作
inc++不是一个原子性操作,可以由读取、加、赋值3步组成,所以结果并不能达到30000。.
解决方法。加锁或者使用Java.current.Automic.*
七、volatile原理
volatile可以保证线程可见性且提供一定有序行,但无法保证原子性。