volatile关键字的作用是什么?相对于对性能影响更大的synchronized关键字(重量级锁),Java提供了一个相对轻量级的解决可见性和顺序问题的方案,那就是使用volatile关键字。由于使用volatile不会引起上下文切换和调度,因此volatile对性能的影响更小,开销也更低。从并发三要素来看,volatile可以保证其修改的变量的可见性和顺序,但不能保证原子性(不能保证完全的原子性,只能保证单个读/写操作是原子的,即,它不能保证复合操作的原子性)。下面将从并发三要素的角度来介绍volatile是如何做到可见有序的。1、volatile如何实现可见性?什么是可见性?可见性是指当多个线程同时访问共享变量时,其他线程可以立即看到一个线程对共享变量的修改(即任何一个线程对共享变量进行操作时,一旦该变量发生变化,所有线程可以立即看到)。1.1可见性示例/***volatile可见性示例*@authorone-wayticket*/publicclassVisibilityDemo{//构造一个共享变量publicstaticbooleanflag=true;//publicstaticvolatilebooleanflag=true;//如果使用volatile修饰可以终止循环publicstaticvoidmain(String[]args){//线程1更改标志newThread(()->{//休眠3秒以确保线程2启动try{TimeUnit.SECONDS.sleep(3);}catch(InterruptedExceptione){e.printStackTrace();}//修改共享变量flag=false;System.out.println("修改成功,当前flag为true");},"一个").start();//线程2获取更新标志终止循环newThread(()->{while(flag){}System.out.println("获取修改标志,终止循环");},"two")。开始();}}当不使用volatile修改flag变量时,运行的程序会进入死循环,也就是说线程1对flag的修改还没有被线程2读到,也就是说这里的flag不是可见性。当使用volatile修改标志变量时,运行的程序会终止循环并打印提示语句,表示线程2已经读取了线程1修改的数据,也就是说volatile修改的变量具有可见性。*1.2volatile如何保证可见性?volatile修改的共享变量标志被一个线程修改后,JMM(JavaMemoryModel)会立即将线程的CPU内存中的共享变量标志刷新回主内存,让线程的CPU内存中的共享变量其他线程将flag缓存失效,这样当其他线程需要访问共享变量flag时,会从主存中获取最新的数据。所以用volatile修饰的变量可以保证可见性。两个问答:为什么会有CPU内存?为了提高处理速度,处理器不直接与内存通信,而是先将系统内存中的数据读取到内部缓存(L1/L2/other)中再进行操作,但操作后的数据不知道什么时候会写回主存。所以如果是普通变量(未修改),什么时候写入主存是不确定的,所以还是有可能读取到旧值,可见性无法保证。各线程的CPU内存如何保持一致性?实施缓存一致性协议(MESI)。MESI在硬件上达成一致:每个处理器通过嗅探总线上传播的数据来检查其CPU内存的值是否过期。当处理器发现自己的缓存行对应的当前处理器的内存地址被修改时,会把当前处理器的缓存行置为无效状态。当处理器修改数据时,它会重新从系统内存(主存)读取数据到处理器缓存(CPU内存)。*1.3volatile实现可见性的原理原理一:锁指令(汇编指令)通过上面例子的Class文件查看汇编指令,会发现变量是否被volatile修饰的区别在于变量被volatile修饰的会有一个lock前缀说明。以lock为前缀的指令会触发两个事件:将当前线程的processorcacheline(CPU内存的最小存储单元,这里可以粗略理解为CPU内存)的数据写入主存(系统内存)并写入回主内存操作会使该内存地址在其他线程的CPU内存中的数据失效(cacheinvalidation)。因此,用volatile修饰的变量在汇编指令中会带有锁前缀指令,这样处理器缓存的数据就会被写回主存,同时使其他线程的处理器缓存的数据失效,这样当其他线程需要使用这些数据时,就会从主存中读取最新的数据,从而实现可见性。原理2:内存屏障(CPU指令)volatile可见性的实现不仅依赖于上述的LOCK指令(汇编指令),还依赖于内存屏障(CPU指令)。为了优化性能,JMM允许编译器和处理器在不改变正确语义的情况下重新排序指令序列。JMM提供内存屏障来防止这种重新排序。下面介绍一类内存屏障:read-writebarriers(用于强制读取或刷新主存中的数据,保证数据的一致性)Storebarriers:当线程修改volatile变量的值时,会插入一个writebarrier,告诉处理器在写入屏障之前将缓存中存储的所有数据同步到主内存。加载屏障:当另一个线程读取一个volatile变量的值时,会在读取前插入一个读屏障,告诉处理器所有在读屏障之后的读操作都可以得到内存屏障之前所有写操作的最新结果。上例中使用javap查看JVM指令时,如果volatile修饰时多了一个ACC_VOLATILE,JVM在将字节码生成机器码时会在相应位置插入内存屏障指令,所以volatile修饰变量的可见性可以通过读写障碍实现性。注意读写屏障的特点:所有的变量(包括没有被volatile修饰的变量)可以一起flush到主存中。虽然这个特性可以让没有被volatile修饰的变量也有所谓的可见性,但是也不要过分依赖这个特性,在编程的时候,需要可见性的变量应该显式的用volatile修饰(当然volatile除外,synchronized、final、各种锁都可以实现可见性,这里就不过多解释了)。2、volatile是如何实现有序的?什么是秩序?sequence是指禁止指令重排序,即保证程序执行代码的顺序与程序编写的顺序一致(程序执行的顺序按照代码)。为什么会发生指令重排序?为了让指令的执行尽可能同时运行,现代计算机使用了指令流水线。如果指令之间没有依赖关系,就可以使流水线的并行度最大化,这样CPU就可以乱序执行没有依赖关系的指令。它可以提高流水线的运行效率,Java编译器可以在不影响最终结果的情况下通过对指令进行重新排序来优化性能。编译器和处理器经常对指令进行重排序,一般分为三种:编译器优化重排序:编译器可以在不改变单线程程序语义的情况下重新安排语句的执行顺序。指令级并行重新排序:现代处理器使用指令级并行来重叠多条指令。如果没有数据依赖,处理器可以改变语句对应机器指令的执行顺序。内存系统重新排序:由于处理器使用高速缓存和读/写缓冲区,加载和存储操作可能看起来是乱序执行的。因此,指令重排序意味着编译器和处理器调整指令的执行顺序,以在不改变数据依赖性的情况下优化程序的性能。这种优化在单线程情况下很好,但在多线程情况下可能会影响程序结果。下面介绍一个多线程下指令重排的例子。2.1订单示例这里我们以单例模式常用的实现DLC双校验为例/***volatile订单示例*@authorone-wayticket*/publicclassSingleton{//使用volatile修饰privatestatic易失单例实例;//私有构造函数privateSingleton(){}//双重检查锁publicstaticSingletongetInstance(){if(instance==null){synchronized(Singleton.class){if(instance==null){instance=newSingleton();}}}返回实例;}}如果你写过单例模式的双锁校验实现,你会发现声明的变量是用volatile修饰的,那这里为什么要用volatile修饰呢?第一个原因是可见性。如果没有volatile修饰,当一个线程给instance赋值时,即instance=newSingleton();,如果其他线程不能及时看到instance更新,就会创建多个单例对象,所以不符合到单例模式的设计思路,所以需要用volatile来装饰。第二个原因是禁止指令重新排序(保证顺序)。为什么需要禁止指令重排序?首先,你需要了解实例一个对象可以分为三个步骤:分配内存空间,初始化对象,将对象引用赋值给变量,因为指令可以重新排序,步骤可能会改变分配内存空间,将对象引用赋值给变量,并初始化对象如果变量没有用volatile修饰,在多线程的情况下可能会出现这样的情况:当一个线程执行第二步时(将对象引用赋值给变量,即此时变量不为null),另外一个线程进入第一步Non-null检查,此时发现变量不为null,直接返回对象,但是对象在由于指令重新排序,这次还没有初始化,即返回了一个未初始化的对象。公开未初始化的变量可能会产生不可预知的后果。所以需要volatile来保证变量的顺序,禁止指令重排序。2.2volatile实现有序性的原则四种内存屏障指令内存屏障禁止指令重排序四种内存屏障指令Java编译器在生成指令时会在适当的位置插入内存屏障,以禁止特定类型的处理器重排序。volatile插入屏障策略在每个volatile写操作之前插入一个StoreStore屏障在每个volatile写操作之后插入一个StoreLoad屏障在每个volatile读操作之后插入一个LoadLoad屏障在每个volatile读操作之后插入一个LoadStore屏障在每个volatile写操作之前和之后插入内存屏障操作,并在每次易失性读取操作后插入两个内存屏障。如何通过内存屏障维持秩序?分析上面的double-checkedlock例子:在没有volatile修饰的情况下,多线程下可能出现的情况如下:为了避免这种情况,在使用volatile修饰变量的时候,会插入一个内存屏障//double-checkedlockpublicstaticSingletongetInstance(){if(instance==null){//首先检查synchronized(Singleton.class){//lockif(instance==null){//第二个检查insertStorStorebarrier//insertbarrierprohibited下面的new操作和read操作被重新排序instance=newSingleton();//创建对象并插入LoadLoad屏障//插入屏障禁止后面的read操作和上面的new操作重新排序}}}returninstance;}这里使用volatile修饰的变量无法避免实例的三步重新排序object,因为volatilekey只能避免多线程间的重排序,无法避免单线程内的重排序。volatile在这里保证顺序的作用是插入barrier之后,必须要等对象的创建完成才能读取。也就是说,线程1要等到对象的整个创建过程完成后才能读取,禁止重排序,这样可以避免返回未初始化的对象并确保有序性。3、为什么volatile不能保证原子性?什么是原子性?原子性是指一个操作或一系列操作是不可分割的,要么全部执行成功,要么一个都不执行(不能中途中断)。为什么volatile不能保证原子性?用例子证明volatile不能保证原子性/***atomicexample*@authorone-wayticket*/publicclassAtomicityDemo{//使用volatile修饰变量publicstaticvolatileinti=0;publicstaticvoidmain(String[]args){ExecutorServicepool=Executors.newFixedThreadPool(1000);//多线程下执行1000次for(intj=0;j<1000;j++){pool.execute(()->i++);}//打印结果System.out.println(i);池。关闭();}}/*Outputresult:997*/正常情况下,打印结果应该是1000,而这里是997,说明这个程序不是线程安全的,可见volatile不能保证原子性。准确的说,volatile不能保证组合操作的原子性,但是可以保证单个操作的原子性。这里的volatile保证了单个操作的原子性,可以应用于使用volatile修饰共享的long或double变量(可以避免分词,想了解的可以参考相关资料,不做过多解释)这里很多)。i++操作是原子的吗?i++实际上不是原子操作。i++其实分为三步:读取i的值,i加1(i+1)和写回i的新值(i=i+1)。这三个步骤中的每一个都是原子操作,但是组合起来就不是原子操作了。如果在多线程的情况下同时执行i++,就会出现数据不一致的问题。所以可以证明,volatile修饰的变量不能保证原子性。i++的原子性可以通过AtomicInteger或者synchronized来保证。4、volatile常见的应用场景?4.1状态标志用volatile修饰一个变量,通过赋不同的常量或值来标识不同的状态。/***布尔值可以用来控制线程的启停*/publicclassMyThreadextendsThread{//状态标志变量privatevolatilebooleanflag=true;//根据状态标志位执行publicvoidrun(){while(flag){//dosomething}}//根据状态标志停止publicvoidstopThread(){flag=false;//改变状态标志变量}}4.2双重检查DLC在多线程编程中,一个对象可能同时被多个线程访问和修改,这个对象可能被重新创建或赋值给另一个对象。这时可以通过volatile修饰变量,保证变量的可见性和顺序。就像单例模式的复查DLC一样,可以使用volatile修改存储单例模式对象的变量。/***单例模式的双重检查方法*/publicclassSingleton{//使用volatile修饰privatestaticvolatileSingleton实例;//私有构造函数privateSingleton(){}//双重检查锁publicstaticSingletongetInstance(){if(instance==null){synchronized(Singleton.class){if(instance==null){instance=newSingleton();}}}返回实例;}}4.3使用开销较低的读写锁volatile可以结合synchronized实现低成本的读写锁。由于volatile可以保证变量的可见性和顺序性,而synchronized可以保证变量的原子性和互斥性,所以它们可以结合使用来实现低成本的读写锁。/***读写锁实现多线程计数器*/publicclassVolatileSynchronizedCounter{//volatile变量privatevolatileintcount=0;//同步方法publicsynchronizedvoidincrement(){count++;//原子操作}publicintgetCount(){returncount;}}使用volatile修饰变量和synchronized修饰方法,让volatile修饰变量可见,写操作会立即对其他线程可见。synchronized修饰的方法保证了count++操作的原子性和互斥性,所以实现的读写锁,读操作不加锁,写操作加锁,减少了开销。
