volatile关键字的作用是什么? 相比于synchronized关键字(重量级锁)对性能影响较大,Java提供了一种较为轻量级的可见性和有序性问题的解决方案,那就是使用volatile关键字。由于使用volatile不会引起上下文的切换和调度,所以volatile对性能的影响较小,开销较低。 从并发三要素的角度看,volatile可以保证其修饰的变量的可见性和有序性,无法保证原子性(不能保证完全的原子性,只能保证单次读写操作具有原子性,即无法保证复合操作的原子性)。 下面将从并发三要素的角度介绍volatile如何做到可见和有序的。1。volatile如何实现可见性? 什么是可见性? 可见性指当多个线程同时访问共享变量时,一个线程对共享变量的修改,其他线程可以立即看到(即任意线程对共享变量操作时,变量一旦改变所有线程立即可以看到)。1。1可见性例子volatile可见性例子author单程车票publicclassVisibilityDemo{构造共享变量publicstaticbooleanflagtrue;publicstaticvolatilebooleanflagtrue;如果使用volatile修饰则可以终止循环publicstaticvoidmain(String〔〕args){线程1更改flagnewThread((){睡眠3秒确保线程2启动try{TimeUnit。SECONDS。sleep(3);}catch(InterruptedExceptione){e。printStackTrace();}修改共享变量flagfalse;System。out。println(修改成功,当前flag为true);},one)。start();线程2获取更新后的flag终止循环newThread((){while(flag){}System。out。println(获取到修改后的flag,终止循环);},two)。start();}}不使用volatile修饰flag变量时,运行程序会进入死循环,也就是说线程1对flag的修改并没有被线程2读到,也就是说这里的flag并不具备可见性。使用volatile修饰flag变量时,运行程序会终止循环,打印提示语句,说明线程2读到了线程1修改后的数据,也就是说被volatile修饰的变量具备可见性。1。2volatile如何保证可见性? 被volatile修饰的共享变量flag被一个线程修改后,JMM(Java内存模型)会把该线程的CPU内存中的共享变量flag立即强制刷新回主存中,并且让其他线程的CPU内存中的共享变量flag缓存失效,这样当其他线程需要访问该共享变量flag时,就会从主存获取最新的数据。 所以通过volatile修饰的变量可以保证可见性。 两点疑问及解答:为什么会有CPU内存?为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1L2其他)后再进行操作,但是操作完后的数据不知道何时才会写回主存。所以如果是普通变量(未被修饰的),什么时候被写入主存是不确定的,所以读取的可能还是旧值,因此无法保证可见性。各个线程的CPU内存是怎么保持一致性的?实现了缓存一致性协议(MESI),MESI在硬件上约定了:每个处理器通过嗅探在总线上传播的数据来检查自己的CPU内存的值是否过期,当处理器发现自己的缓存行对应的内存地址被修改了,就会将当前处理器的缓存行设置为无效状态。当处理器对该数据进行修改操作时,会重新从系统内存(主存)中把数据读到处理器缓存(CPU内存)里。1。3volatile实现可见性的原理 原理一:Lock指令(汇编指令) 通过上面的例子的Class文件查看汇编指令时,会发现变量有无被volatile修饰的区别在于被volatile修饰的变量会多一个lock前缀的指令。 lock前缀的指令会触发两个事件:将当前线程的处理器缓存行(CPU内存的最小存储单元,这里可以大致理解为CPU内存)的数据写回到主存(系统内存)中写回主存的操作会使其他线程的CPU内存中该内存地址的数据无效(缓存失效) 所以使用volatile修饰的变量在汇编指令中会有lock前缀的指令,所以会将处理器缓存的数据写回主存中,同时使其他线程的处理器缓存的数据失效,这样其他线程需要使用数据时,会从主存中读取最新的数据,从而实现可见性。 原理二:内存屏障(CPU指令) volatile的可见性实现除了依靠上述的LOCK指令(汇编指令)还依靠内存屏障(CPU指令)。 为了性能优化,JMM在不改变正确语义的前提下,会允许编译器和处理器对指令序列进行重排序。JMM提供了内存屏障阻止这种重排序。 这里介绍的是内存屏障中的一类:读写屏障(用于强制读取或刷新主存的数据,保证数据一致性)Store屏障:当一个线程修改了volatile变量的值,它会在修改后插入一个写屏障,告诉处理器在写屏障之前将所有存储在缓存中的数据同步到主内存。Load屏障:当另一个线程读取volatile变量的值,它会在读取前插入一个读屏障,告诉处理器在读屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果。 对上面的例子使用javap查看JVM指令时,如果被volatile修饰时多一个ACCVOLATILE,JVM把字节码生成机器码时会在相应位置插入内存屏障指令,因此可以通过读写屏障实现volatile修饰变量的可见性。 注意读写屏障的特点:可以将所有变量(包括不被volatile修饰的变量)一起全部刷入主存,尽管这个特性可以使未被volatile修饰的变量也具备所谓的可见性,但是不应该过于依赖这个特性,在编程时,对需要要求可见性的变量应当明确的用volatile修饰(当然除了volatile,synchronized、final以及各种锁都可以实现可见性,这里不过多说明)。2。volatile如何实现有序性? 有序性是什么? 有序性指禁止指令重排序,即保证程序执行代码的顺序与编写程序的顺序一致(程序执行顺序按照代码的先后顺序执行)。 为什么会发生指令重排序? 现代计算机为了能让指令的执行尽可能的同时运行起来,采用指令流水线的方式,若指令之间不具有依赖,可以使流水线的并行最大化,所以CPU对无依赖的指令可以乱序执行,这样可以提高流水线的运行效率,在不影响最后结果的情况下,Java编译器可以通过指令重排序来优化性能。 编译器和处理器常常会对指令做重排序,一般分为三种类型:编译器优化重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。指令级并行重排序:现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。内存系统重排序:由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。 所以指令重排序是指编译器和处理器为了优化程序的性能,在不改变数据依赖性的情况下,调整指令的执行顺序。 这种优化在单线程情况下没有问题,但是在多线程情况下可能会导致影响程序结果。接下来将介绍一个多线程下指令重排的例子。2。1有序性例子 这里以单例模式的常用实现方式DLC双重检查为例子volatile有序性例子author单程车票publicclassSingleton{使用volatile进行修饰privatestaticvolatileSingletoninstance;私有化构造器privateSingleton(){}双重检查锁publicstaticSingletongetInstance(){if(instancenull){synchronized(Singleton。class){if(instancenull){instancenewSingleton();}}}returninstance;}} 如果写过单例模式的双重锁检查实现方式,会发现声明的变量被volatile修饰,那么为什么这里需要使用volatile修饰呢? 第一个原因是可见性,如果没有volatile修饰的话,当一个线程给instance赋值即instancenewSingleton();后,其他线程如果无法及时看到instance更新,会导致创建多个单例对象,这样就不符合单例模式设计思想了,所以需要使用volatile修饰。 第二个原因则是禁止指令重排序(保证有序性),为什么需要禁止指令重排呢? 首先需要了解实例一个对象可以分为三个步骤:分配内存空间初始化对象将对象引用赋值给变量 由于指令可以进行重排序,所以步骤可能发生变化变为分配内存空间将对象引用赋值给变量初始化对象 如果未使用volatile修饰变量的话,多线程情况下可能出现这样的情况: 一个线程在执行第二步(将对象引用赋值给变量,即此时变量不为null)时,而另一个线程进入第一次非空检查,此时发现变量不为null,直接返回对象,但是此时的对象由于指令重排序的原因并未进行初始化,即返回了一个未初始化的对象。将一个未初始化的变量暴露出来会导致不可预料的后果。 所以需要volatile保证变量有序性,禁止指令重排序。2。2volatile实现有序性的原理 内存屏障的四种指令 内存屏障中禁止指令重排序的内存屏障的四种指令 指令 说明 LoadLoad屏障 保证在该屏障之后的读操作,不会被重排序到该屏障之前的读操作 StoreStore屏障 保证在该屏障之后的写操作,不会被重排序到该屏障之前的写操作,并且该屏障之前的写操作已被刷入主存 StoreLoad屏障 保证在该屏障之后的读操作,能够看到该屏障之前的写操作对应变量的最新值 LoadStore屏障 保证在该屏障之后的写操作,不会被重排序到该屏障之前的读操作 Java编译器会在生成指令时在适当位置插入内存屏障来禁止特定类型的处理器重排序。 volatile的插入屏障策略在每个volatile写操作的前面插入一个StoreStore屏障在每个volatile写操作的后面插入一个StoreLoad屏障在每个volatile读操作的后面插入一个LoadLoad屏障在每个volatile读操作的后面插入一个LoadStore屏障 即在每个volatile写操作前后分别插入内存屏障,在每个volatile读操作后插入两个内存屏障。 如何通过内存屏障保持有序性? 分析上面的双重检查锁例子: 不加volatile修饰时,多线程下可能出现的情况是这样的: 为了避免这种情况,使用volatile修饰变量时,会插入内存屏障双重检查锁publicstaticSingletongetInstance(){if(instancenull){第一次检查synchronized(Singleton。class){加锁if(instancenull){第二次检查插入StorStore屏障插入屏障禁止下面的new操作和读取操作重排序instancenewSingleton();创建对象插入LoadLoad屏障插入屏障禁止下面的读取操作和上面的new操作重排序}}}returninstance;} 这里使用volatile修饰变量并不能避免实例对象的三个步骤重排序,因为volatile关键只能避免多个线程之间的重排序,不能避免单个线程内部的重排序。 这里volatile保证有序性的作用在于插入屏障之后必须等创建对象完成后才能进行读取操作,也就是说需要线程1的创建对象整个步骤完成后才会让线程2进行读取,禁止了重排序,这样就避免了返回一个未初始化的对象,保证了有序性。3。volatile为什么不能保证原子性? 什么是原子性? 原子性指一个操作或一系列操作是不可分割的,要么全部执行成功,要么全部不执行(中途不可被中断)。 为什么volatile不能保证原子性呢? 通过一个例子来证明volatile不能保证原子性原子性例子author单程车票publicclassAtomicityDemo{使用volatile修饰变量publicstaticvolatileinti0;publicstaticvoidmain(String〔〕args){ExecutorServicepoolExecutors。newFixedThreadPool(1000);多线程情况下执行1000次for(intj0;j1000;j){pool。execute(()i);}打印结果System。out。println(i);pool。shutdown();}}输出结果:997 正常情况下,打印结果应该为1000,但是这里却是997,说明这段程序并不是线程安全的,可以看出volatile无法保证原子性。 准确来说应该是volatile无法保证复合操作的原子性,但能保证单个操作的原子性。 这里volatile保证单个操作的原子性可以应用于使用volatile修饰共享的long或者double变量(可以避免字分裂情况,具体想要了解到可以查阅相关资料这里不做过多说明)。 i操作是原子操作吗? i其实不是原子操作,实际上i分为三个步骤:读取i的值将i自增1(i1)写回i的新值(ii1) 这三个步骤每一步都是原子操作,但是组合起来就不是原子操作了,在多线程情况下同时执行i,会出现数据不一致性的问题。 所以可以证明volatile修饰的变量无法保证原子性。 可以通过AtomicInteger或者synchronized来保证i的原子性。4。volatile常见的应用场景?4。1状态标志位 使用volatile修饰一个变量通过赋值不同的常数或值来标识不同的状态。可以通过布尔值来控制线程的启动和停止publicclassMyThreadextendsThread{状态标志变量privatevolatilebooleanflagtrue;根据状态标志位来执行publicvoidrun(){while(flag){dosomething}}根据状态标志位来停止publicvoidstopThread(){flagfalse;改变状态标志变量}}4。2双重检查DLC 在多线程编程下,一个对象可能会被多个线程同时访问和修改,而且这个对象可能会被重新创建或者赋值为另一个对象。此时可以通过volatile来修饰该变量,保证该变量的可见性和有序性。 就如单例模式的双重检查DLC可以通过volatile来修饰从存储单例模式对象的变量。单例模式的双重检查方式publicclassSingleton{使用volatile进行修饰privatestaticvolatileSingletoninstance;私有化构造器privateSingleton(){}双重检查锁publicstaticSingletongetInstance(){if(instancenull){synchronized(Singleton。class){if(instancenull){instancenewSingleton();}}}returninstance;}}4。3较低开销的读写锁 使用volatile结合synchronized实现较低开销的读写锁,由于volatile可以保证变量的可见性和有序性,而synchronized可以保证变量的原子性和互斥性,可以结合使用实现较低开销的读写锁。读写锁实现多线程下的计数器publicclassVolatileSynchronizedCounter{volatile变量privatevolatileintcount0;synchronized方法publicsynchronizedvoidincrement(){count;原子操作}publicintgetCount(){returncount;}} 使用volatile修饰变量,synchronized修饰方法,这样volatile修饰变量具有可见性,写操作会被其他线程立刻可见,synchronized修饰方法保证count操作的原子性和互斥性,这样实现的读写锁,读操作无锁,写操作有锁,降低了开销。