什么是volatile关键字volatile是Java中用来修饰变量的关键字,可以保证变量的可见性和顺序,但不能保证原子性。更准确地说,volatile关键字只能保证单个操作的原子性,比如x=1,而不能保证复合操作的原子性,比如x++,这为Java提供了一种轻量级的同步机制:保证为volatile修饰的共享变量对所有线程总是可见的,也就是说,当一个线程修改一个被volatile修饰的共享变量的值时,新的值总是能立即被其他线程知道。与synchronized关键字(synchronized通常被称为重量级锁)相比,volatile更轻量,开销也小,因为它不会引起线程上下文切换和调度。GuaranteedVisibility可见性:是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程可以立即看到修改后的值。让我们看一个例子:publicclassVisibilityTest{privatebooleanflag=true;publicvoidchange(){flag=false;System.out.println(Thread.currentThread().getName()+",modifiedflag=false");}publicvoidload(){System.out.println(Thread.currentThread().getName()+",开始执行....");诠释我=0;while(flag){i++;}System.out.println(Thread.currentThread().getName()+",循环结束");}publicstaticvoidmain(String[]args)throwsInterruptedException{VisibilityTesttest=newVisibilityTest();//ThreadthreadA模拟数据加载场景ThreadthreadA=newThread(()->test.load(),"线程A");threadA.start();//让threadA执行一段时间Thread.sleep(1000);//线程threadB修改共享变量flagThreadthreadB=newThread(()->test.change(),"threadB");threadB.start();}}其中:threadA负责循环,threadB负责修改共享变量测量flag,如果flag=false,threadA会结束循环,但是上面的例子会死循环!原因是threadA不能立即读取共享变量flag的修改值。我们只需要privatevolatilebooleanflag=true;并添加volatile关键字threadA以立即退出循环。其中,Java中的volatile关键字提供了一个功能:即volatile修饰的变量P被修改后,JMM会强制线程本地内存中的变量P立即刷新到主内存中,造成其他线程tovolatile变量P缓存失效,也就是说当其他线程使用volatile变量P时,最新的数据从主存中刷新。普通变量的值在线程之间传递时,一般是通过主内存以共享内存的形式实现;因此可以使用volatile来保证多线程运行时变量的可见性。除了volatile,Java中的synchronized和final关键字以及各种Locks也可以实现可见性。如果加了锁,当线程进入synchronized代码块时,线程获取到锁,清除本地内存,然后将共享变量的最新值从主内存复制到本地内存作为副本,执行代码,并将修改后的拷贝值Flush到主存,最后线程释放锁。有保证的有序性有序性,顾名思义,就是程序执行的顺序遵循代码执行的顺序。但是,为了让指令的执行在现代计算机的CPU中尽可能同时运行,提高计算机的性能,采用了指令流水线。一条CPU指令的执行过程可以分为4个阶段:取指、译码、执行和回写。这4个阶段分别由4个独立的物理执行单元完成。理想的情况是:指令之间没有依赖关系,可以最大限度地提高流水线的并行度。但是,如果两条指令之间存在依赖关系,如数据依赖、控制依赖等,则后一条语句必须等到前一条指令完成。开始。因此,为了提高流水线的运行效率,CPU会对独立的前导指令进行适当的乱序和调度。也就是说,现代计算机中的CPU是乱序执行指令的另一面。只要程序的运行结果不变,Java编译处理器就可以通过指令重排来优化性能。但是,重新排序可能会影响本地处理器高速缓存与主内存交互的方式,可能会在多线程情况下导致“细微”错误。指令重排一般可分为以下三种类型:编译器优化重排,编译器可以在不改变单线程程序语义的情况下,重新安排语句的执行顺序。Instruction-levelparallelreordering,现代处理器使用指令级并行来重叠和执行多条指令。如果没有数据依赖,处理器可以改变语句对应机器指令的执行顺序。内存系统重新排序,由于处理器使用高速缓存和读/写缓冲区,这使得加载和存储操作看起来乱序执行。这不是对指令的显式重新排序,只是指令的执行由于缓存的原因看起来乱序了。从Java源码到最终执行的指令序列,一般会经历以下三种重排序:编译器优化重排序-指令级并行重排序-内存系统重排序-最终执行指令排序变量初始化赋值我们来看一个例子,让volatile关键字禁止指令重排的作用大家都明白了:inti=0;整数j=0;诠释k=0;我=10;j=1;对于上面的代码,我们正常的执行流程是:InitializeiInitializejInitializekiAssignmentjAssignment但是由于指令重排序的问题,代码的执行顺序可能不是代码写的时候的顺序。该语句可能的执行顺序如下:initializationiiassignmentinitializationjjassignmentinitializationkinstructionrearrangement对于非原子操作,拆分成它们的原子操作可以按执行顺序重新排列而不影响最终结果。表现。指令重排不会影响单个线程的执行结果,但会影响多个线程并发执行结果的正确性。但是当我们用volatile修改变量k时:inti=0;整数j=0;易失性intk=0;我=10;j=1;这样就保证了上面代码的执行顺序:变量i和j的初始化,在volatileintk=0之前,变量i和j的赋值操作在volatileintk=0之后Lazysingleton--doublechecklockvolatileversion我们可以使用volatile关键字来防止读写指令围绕volatile变量进行重排,这种操作通常称为内存屏障(memorybarrier)。(包括非易失性变量)也被刷新到主内存。当线程读取volatile变量时,它还会读取所有其他变量(包括非volatile变量)并将它们与volatile变量一起刷新到主内存。虽然这是一个重要的特性,但我们不应该过分依赖这个特性来“自动”让周围的变量变得易变。如果我们想让一个变量是volatile,那么在写程序的时候就需要非常显式地使用volatile。要修改的关键字。不能保证原子性volatile关键字不能保证原子性。更准确地说,volatile关键字只能保证单个操作的原子性,比如x=1,而不能保证复合操作的原子性,比如x++的所谓原子性:一个或多个操作作为一个整体,要么全部执行,要么都不执行,执行过程中不会被线程调度机制打断;任意上下文切换(contextswitch)int=0;//语句1,单操作,原子操作i++;//语句2,复合操作,非原子操作其中:语句2i++实际执行的是Java中的流程,分为3步:1.i从局部变量表(内存)中取出,2.是压入操作栈(寄存器),操作栈自增3.局部变量表用栈顶值更新(寄存器更新写入内存))执行以上3步时,线程切换可以执行,也可以被其他线程的这3步打断,所以语句2不是原子操作volatile版本再看一个例子:publicclassTest1{publicstaticvolatileintval;publicstaticvoidadd(){for(inti=0;i<1000;i++){val++;}}publicstaticvoidmain(String[]args)throwsInterruptedException{Threadt1=newThread(Test1::add);Threadt2=newThread(Test1::add);t1.开始();t2.开始();t1.join();//等待线程终止t2.join();System.out.println(val);}}2个线程每次循环2000次,每次+1,如果volatile关键字能保证原子性,预期结果为2000,但实际结果为:1127,且多次执行结果不同,可以发现volatile关键字不能保证原子性。在synchronized版本中,我们可以使用synchronized关键字来解决上面的问题:publicclassSynchronizedTest{publicstaticintval;publicsynchronizedstaticvoidadd(){for(inti=0;i<1000;i++){val++;}}publicstaticvoidmain(String[]args)throwsInterruptedException{Threadt1=newThread(SynchronizedTest::add);线程t2=新线程(SynchronizedTest::add);t1.开始();t2.开始();t1.join();//等待线程终止t2.join();System.out.println(val);}}运行结果:2000Lockversion我们也可以通过加锁来解决上面的问题:publicclassLockTest{publicstaticintval;staticLocklock=newReentrantLock();publicstaticvoidadd(){for(inti=0;i<1000;i++){lock.lock();//锁尝试{val++;}catch(Exceptione){e.printStackTrace();}finally{lock.unlock();//解锁}}}publicstaticvoidmain(String[]args)throwsInterruptedException{Threadt1=newThread(LockTest::add);Threadt2=newThread(LockTest::add);t1.开始();t2.开始();t1。join();//等待线程终止t2.join();System.out.println(val);}}运行结果:2000Atomic版本i++Java从JDK1.5包(以下简称Atomic包)中提供java.util.concurrent.atomic,该包中的原子操作类,依赖CAS循环保证其原子性,这是更新变量的一种简单、高效且线程安全的方式。这些类可以保证在多线程环境下,当一个线程在执行一个原子方法时,不会被其他线程打断,而其他线程就像自旋锁一样,等到该方法执行完成后,JVM才会选择等待队列中的一个线程实现。我们使用atomic包来解决volatile原子性的问题:publicclassAtomicTest{publicstaticAtomicIntegerval=newAtomicInteger();publicstaticvoidadd(){for(inti=0;i<1000;i++){val.getAndIncrement();}}publicstaticvoidmain(String[]args)throwsInterruptedException{Threadt1=newThread(AtomicTest::add);Threadt2=newThread(AtomicTest::add);t1.开始();t2.开始();t1.join();//等待线程终止t2.join();System.out.println(val);}}运行结果:2000,如果我们维护现有项目,如果遇到volatile变量,最好换成Atomic变量,除非你真的了解volatile。Atomic就不在那展开了,先挖坑,再补volatile的原理。当你仔细阅读上面的惰性单例——双重检查锁定volatile版本时,你会发现volatile关键字修饰变量后,我们将对其进行反汇编。后面你会发现锁前缀指令多了。锁前缀指令在汇编中的作用如下:修饰后的汇编指令变成“原子的”,修饰后的汇编指令提供了“内存屏障”的作用(锁指令不是内存屏障)内存屏障的主要分类:1.一种是可以强制对主存进行读取和刷新的内存屏障,称为Loadbarrier和Storebarrier2.另一种是禁止指令重排序的内存屏障,主要包括四种屏障分别称为LoadLoadbarrier,StoreStorebarrier、LoadStore屏障和StoreLoad屏障。这四个barrier的具体作用是:LoadLoadbarrier:(commandLoad1;LoadLoad;Load2),在访问Load2要读取的数据以及后续的读操作之前,保证Load1要读取的数据已经被读取。LoadStorebarrier:(commandLoad1;LoadStore;Store2),保证Load1要读取的数据在Store2和后续写操作被flushout之前被读取。StoreStorebarrier:(instructionStore1;StoreStore;Store2),在执行Store2和后续写操作之前,保证Store1的写操作对其他处理器可见。StoreLoadbarrier:(instructionStore1;StoreLoad;Load2),在Load2和后续所有读操作执行之前,保证对Store1的写入对所有处理器可见。它的开销是四个障碍中最大的。在大多数处理器的实现中,这个屏障是一个通用屏障,它同时具有其他三个内存屏障的功能。对于volatile操作,操作步骤如下:每次volatile写前,插入一个StoreStore,写完后插入一个StoreLoad,每次volatile读前插入一个LoadLoad,读完后插入一个LoadStore。让我们总结一下。用volatile关键字修饰变量后,主要有哪些变化?:1.当一个线程修改一个被volatile修饰的变量时,当修改后的变量写回主存时,其他线程可以立即看到最新的值。即volatile关键字保证了并发的可见性。使用volatile关键字修饰一个共享变量后,每个线程在要操作该变量时,都会将该变量从主存复制到本地内存作为副本,但是当该线程操作完变量副本后,会强制修改后的值立即写入主存。然后利用CPU总线嗅探机制通知其他线程该变量的所有副本均无效(在CPU层,将一个处理器的缓存写回内存会使其他处理器的缓存行无效)。如果其他线程需要该变量,则必须再次从主存中读取。2、在x86架构中,volatile关键字底层包含带锁前缀的指令,与修改后的汇编指令一起提供“内存屏障”作用,禁止指令重排序,保证并发顺序,保证执行一些特定的操作顺序,使得cpu必须按顺序执行指令,即指令重新排序时,后面的指令不会排列到内存屏障之前的位置,前面的指令也不会排列到内存屏障的背面;当使用内存屏障指令时,它之前的操作已经全部完成;3、volatile关键字不能保证原子性。更准确地说,volatile关键字只能保证单个操作的原子性,比如x=1,而不能保证复合操作的原子性,比如x++。可能有人会问,赋值操作是原子操作,本质上是原子的,那么用volatile修饰有什么意义呢?当Java数据类型足够大时(Java中long和double类型都是64位),写变量的过程分两步进行,就会出现Wordtearing(分词)。JVM被允许将64位量的读取和写入作为两个单独的32位操作来执行,这增加了读取和写入期间上下文切换的可能性,并且在多线程的情况下可能会损坏值。在没有任何其他保护的情况下,使用volatile修饰符定义long或double变量可防止分词
