概念回顾首先回顾一下我们之前讲过的基本概念:内存可见性“内存可见性是指线程之间的可见性,当一个线程修改共享变量时,另一个线程可以读取到修改后的值”。为了优化程序性能,重新排序对原始指令执行顺序进行了优化和重新排序。重排序可以发生在多个阶段,比如编译重排序、CPU重排序等。happens-before遵循happens-before规则,JVM可以保证多线程间的指令顺序满足执行的预期。volatile保证了变量的“内存可见性”。禁止对volatile变量和普通变量进行“重新排序”。那么这个内存可见性过程是什么样的呢?具体代码我之前也给大家演示过,这里直接给大家总结一下:所谓内存可见性,当一个线程写入一个被volatile修饰的变量时,会立即刷新本地内存中的共享变量到主存,同理,当进行读操作时,本地内存会立即失效,从主存中读取共享变量的值。此时,volatile与锁具有相同的内存效应,写入volatile变量与释放锁具有相同的内存语义,读取volatile变量与获取锁具有相同的内存语义。重排禁令呢?在JSR-133之前的旧Java内存模型中,允许对volatile变量和普通变量进行重新排序。想一想,如果可以重新排列会怎样?我们假设有两个线程A和B,一个变量a被volatile修饰,一个普通变量b没有被修饰,见如下代码:volatilea=1;整数b=2;publicvoidwriter(){a=1;b=3;}publicvoidreader(){if(a==1){System.out.println(b);}}线程A执行writer方法,先设置a为1,此时线程B操作reader方法,此时判断a=1,然后输出b=2,线程A多b操作设置为3、其实最后的结果应该是b=3,所以这里的重排可能会导致普通变量的误读。为了提供比锁更轻量级的“线程间通信机制”,JSR-133决定增强volatile的内存语义:严格限制编译器和处理器对volatile变量和普通变量的重新排序。那么怎么禁止呢?答案就是通过内存屏障,也许你听说过这个概念,下面我们一起来看看吧。内存屏障什么是内存屏障?在计算机中,主要有两种,一种是读屏障(LoadBarrier)和写屏障(StoreBarrier)。内存屏障有两个用途:它防止屏障两侧的指令重新排序。强制将写缓冲区/缓存中的脏数据写回主存,或者使缓存中的相应数据失效。(这里的缓存是指cpu的L1、L2等多级缓存)。我们写的代码最终都会经过编译器,那么编译器是如何实现这个过程的呢?当编译器生成字节码时,它会在指令序列中插入内存屏障,以禁止某些类型的处理器重新排序。在Java中,JMM内存屏障插入策略可以保证程序在各个平台处理器下的volatile内存语义是正确的。具体策略:在每次volatile写操作前插入一个writebarrier。在每个易失性写操作之后插入一个写屏障。在每个易失性读取操作之后插入一个读取屏障。在每个易失性读取操作之后插入一个读取屏障。volatile和普通变量的重排序规则:如果第一个操作是volatile读,那么无论第二个操作是什么,都不能重排序。如果第二个操作是volatile写,不管第一个操作是什么,都不能重新排序。如果第一个操作是易失性写入而第二个操作是易失性读取,则无法重新排序。那么如果第一个操作是普通变量读,第二个是volatile读,是否可以重排呢?答:是的。相信理解了以上概念后,你应该对volatile的使用场景有了一定的了解。volatile可以保证内存可见性,禁止重排序。它具有与锁相同的内存语义,也称为轻量级锁。volatile只保证对单个volatile变量的读/写的原子性,而锁可以保证整个“临界区代码”执行的原子性。所以锁要高级一点。但这并不意味着volatile不好。作为一个轻量级的锁,在某些场景下还是很有用的。下面以双锁校验单例模式为例。首先我们看一下常见的单例模式:classSingleton{privatestaticSingletoninstance;privateSingleton(){}publicSingletongetInstance(){if(instance==null){instance=newSingleton();}返回实例;}}在单线程模式下可以这样做,没有什么问题,但是在多线程模式下就不行了,所有我们需要加锁,所以双锁下的实现check:classSingleton{privatestatic单例实例;privateSingleton(){}publicstaticSingletongetInstance(){if(instance==null){synchronized(Singleton.class){if(instance==null){instance=newSingleton();}}}返回实例;}}这样真的可以吗?怀疑?我们知道我们在新手的时候,主要做三件事:分配内存变量、赋值和初始化对象的过程,可能会导致指令重排,你可能会说里面加锁了。上一节介绍了顺序一致性模型,在上面我们说过临界区的代码可以同步方式重新排序,所以这里还是有重新排序的可能的,所以最后的流程可能是这样的。线程A执行内存分配->变量赋值,线程B执行判断实例不为null,开始访问对象。其实这个对象还没有初始化,所以这个时候,我们需要加上volatile。classSingleton{privatestaticvolatileSingleton实例;privateSingleton(){}publicstaticSingletongetInstance(){if(instance==null){synchronized(Singleton.class){if(instance==null){instance=newSingleton();}}}返回实例;}}这样,在多线程环境下,它的安全性就可以得到保证。结束语这一段的内容可能没有之前那么容易理解,比较抽象,所以这篇文章也有不足之处。可以自己查一些资料,综合理解,不要死记硬背概念。在本节中,我们提到了锁的概念。
