volatile是Java并发编程的重要组成部分,也是面试常见问题之一。它有两个主要功能:确保内存可见性和禁止指令重排序。让我们仔细看看这两个函数。内存可见性说到内存可见性,就不得不提到Java内存模型。Java内存模型(JavaMemoryModel)简称为JMM。主要用来屏蔽不同硬件和操作系统的内存访问差异,因为不同的硬件在不同的操作系统下内存访问存在一定差异。这种差异会导致相同的代码在不同的硬件和不同的操作系统下有不同的行为。Java内存模型就是为了解决这个差异。统一同一代码在不同硬件、不同操作系统下的差异。Java内存模型规定所有的变量(实例变量和静态变量)都必须存放在主内存中,每个线程也会有自己的工作内存。线程的工作内存保存线程使用的变量和主存的内存。Copycopy,线程对变量的操作是在工作内存中进行的。线程不能直接在主存中读写变量,如下图:但是Java内存模型会带来一个新的问题,就是内存可见性的问题,即当一个线程修改共享变量时在主存中设置值后,其他线程无法感知到该值被修改,它会一直使用自己工作内存中的“旧值”,这样程序的执行结果不符合我们的预期.这就是内存可见性的问题。我们用下面的代码演示了这个问题:privatestaticbooleanflag=false;publicstaticvoidmain(String[]args){Threadt1=newThread(newRunnable(){@Overridepublicvoidrun(){while(!flag){}System.out.println("终止执行");}});t1.开始();Threadt2=newThread(newRunnable(){@Overridepublicvoidrun(){try{Thread.sleep(1000);}catch(InterruptedExceptione){e.printStackTrace();}System.out.println("Set标志=真”);标志=真;}});t2.start();}上面代码我们预期的结果是,线程1执行1s后,线程2修改flag变量为true,然后线程1终止执行。但是由于线程1无法感知到标志变量被修改,也就是内存可见性问题,所以会导致线程1一直执行下去,最终我们看到的结果是这样的:如何解决上述问题呢?只需将volatile添加到变量标志中即可。具体实现代码如下:privatevolatilestaticbooleanflag=false;publicstaticvoidmain(String[]args){Threadt1=newThread(newRunnable(){@Overridepublicvoidrun(){while(!flag){}System.out.println("终止执行");}});t1.开始();Threadt2=newThread(newRunnable(){@Overridepublicvoidrun(){try{Thread.sleep(1000);}catch(InterruptedExceptione){e.printStackTrace();}System.out.println("Set标志=真”);标志=真;}});t2.start();}上述程序的执行结果如下图所示:禁止命令Reordering指令重排序是指编译器或CPU为了优化程序的执行性能而对指令进行重新排序的一种手段。指令重排序的初衷是好的,但是在多线程执行中,如果指令排序可能会导致程序执行出错。单例模式下最典型的指令重排序问题之一,比如下面的问题代码:publicclassSingleton{privateSingleton(){}privatestaticSingletoninstance=null;publicstaticSingletongetInstance(){if(instance==null){//①synchronized(Singleton.class){if(instance==null){instance=newSingleton();//②}}}返回实例;}}上述问题出现在代码行②“instance=newSingleton();”,这行代码看似只是一个创建对象的过程,但它的实际执行分为以下三个步骤:创建内存空间。在内存空间中初始化对象Singleton。将内存地址赋给实例对象(执行这一步后,实例不等于null)。如果这个变量不是volatile,那么线程1在执行到上面代码的点②时可能会进行指令重排序,将原来的1,2,3的执行顺序重新排序为1,3,2。但是,在特殊情况下,线程1执行完步骤3后,如果线程2执行到上述代码的①点,判断实例对象不为null,但是线程1还没有实例化该对象。那么线程2会得到一个实例化的“半个”对象,这会导致程序执行错误,这就是为什么要在私有变量中加入volatile的原因。要把上面的单例模式变成线程安全的程序,需要对实例变量加上volatile修饰。其最终实现代码如下:publicclassSingleton{privateSingleton(){}//使用volatile禁止顺序重排序privatestaticvolatileSingletoninstance=null;//【主要是因为这行代码变了】publicstaticSingletongetInstance(){if(instance==null){//①synchronized(Singleton.class){if(instance==null){instance=newSingleton();//②}}}返回实例;}}总结volatile是Java并发编程的重要组成部分。它有两个主要功能:确保内存可见性和禁止指令重排序。volatile常用于一次写入多次读取的场景,比如CopyOnWriteArrayList集合,在运行过程中复制所有数据并锁定写入操作。修改后,使用setArray方法将数组赋值给更新后的值。使用volatile允许读取线程快速通知数组已被修改,而无需重新排序指令。操作完成后,可以被其他线程看到。
