对《深入理解Java虚拟机——周志明》中第12章的总结概述硬件效率与一致性  为了解决处理器与内存之间的速度矛盾,引入了基于高速缓存的存储交互。  但高速缓存的引入也带来了新的问题:缓存一致性,即多处理器中,每个处理器有各自的高速缓存,而他们又共享同一主内存。当多个处理器的运算任务额都涉及同一块主存区域的时候,将可能导致各自的缓存数据不一致,如果真的发生这种情况,那么同步回到主存时以谁的缓存数据为准呢?  为了解决一致性的问题,需要各个处理器在访问缓存时都遵循一些协议,在读写时要根据协议来进行操作。在本章中将会多次提到“内存模型”一词,可以理解为在特定的操作协议下,对特定的内存或高速缓存进行读写访问时的过程抽象。不同的物理机器可以拥有不同的内存模型。而Java虚拟机也拥有自己的内存模型,并且在这里的内存访问操作与硬件的访问操作具有很高的可比性。Java内存模型  Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。在JDK1.5(实现了JSR-133)发布后,Java内存模型已经成熟和完善起来了。主内存和工作内存  Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。这里的变量不包括局部变量和参数,因为其实线程私有的,不会被共享(但局部引用变量所指向的对象仍然是可共享的),自然不会存在竞争问题。  Java内存模型规定了所有的变量都存储在主内存中(此处的主内存与介绍物理硬件时的主内存名字一样,两者也可以相互类比,但此处仅是虚拟机内存的一部分),每条线程还有自己的工作内存(可与高速缓存类比),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量(包括volatile变量也是这样)。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。内存间交互操作  关于主内存和工作内存之间具体的交互协议:即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节。Java内存模型中定义了以下8种操作来完成,虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的(对于double、long类型的变量来说,load、store、read和write操作在某些平台上允许有例外)。lockunlockreadloaduseassignstorewrite       如果要把一个变量从主内存复制到工作内存,那么就要顺序地执行read和load操作,如果要把变量从工作内存同步回主内存,就要顺序地执行store和write操作。注意:Java内存模型只要求上述两个操作必须按顺序执行,而没有保证是连续执行。也就是说read和write之间是可以插入其他指令的,如对主内存的变量a、b进行访问时,一种可能出现的顺序是read a、read b、load b、load a。除此之外,Java内存模型还规定了在执行上述8中基本操作时所必须满足如下规则:不允许read和load、store和write之一单独出现,即不允许一个变量从主内存读取了但工作内存不接收,或者是从工作内存发起回写了但主内存不接受的情况出现。不允许一个线程丢弃它最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。不允许一个线程无原因地(没有发生过assign操作)把数据从线程的工作内存同步回主内存中。一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未初始化(load或assign)的变量。换言之,就是对于一个变量实施use、store操作之前,必须先执行过assign和load操作。一个变量在同一时刻只允许一个线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。如果对一个变量执行lock操作,那将会清空工作内存中的此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不许去unlock一个被其他线程锁定住的变量。对一个变量执行unlock操作之前必须先把该变量同步回主内存中(执行store、write操作)。       这八种内存访问操作以及上述规则限定,再加上稍后介绍的对volatile的一些特殊规定,就已经完全确定了Java程序中哪些内存访问操作在并发下是安全的。由于这种定义相当严谨但确实比较繁琐,实践起来比较麻烦,所以后面会介绍这种定义的一个等效判断原则————先行发生原则,用来确定一个访问在并发环境下是否安全。对于volatile型变量的特殊规则  关键字可以说是Java虚拟机提供的最轻量级的同步机制,但它并不是很容易完全被正确、完整地理解。以至于许多程序员都习惯不去使用它,遇到需要处理多线程数据竞争的时候一律使用synchronized来进行同步。  当将一个变量定义为volatile之后,它将具备两种特性,第一是保证此变量对所有线程的可见性,这里的可见性是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。而普通变量不能保证这一点,普通变量的值在线程间传递均需要通过主内存来完成,例如,线程A修改一个普通变量的值,然后向主内存进行回写,另一条线程B在线程A回写完成了之后在从主内存进行读取操作,新变量的值才会对线程B可见。第二是禁止指令重排序优化,普通的变量仅仅会保证在该方法执行的过程中所有依赖赋值的结果都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。因为在一个线程的方法执行过程中无法感知这点,这就是Java内存模型当中所描述的“线程内表现为串行”的语义(Within Thread As-If Serial Semantics)。可见性  关于volatile可见性:volatile变量是对所有线程可见的,对volatile变量所有的写操作都能立刻反映到其他线程之中,换言之,volatile变量在各个线程中是一致的。但一致并不代表基于volatile变量的运算在并发下是安全的。volatile变量在各个线程的工作内存中不存在一致性的问题(在各个线程的工作内存中,volatile变量也可以存在不一致的情况,但在每次使用之前都要先刷新,执行引擎看不到不一致的情况,所以可以认为不存在一致性的问题),但Java里面的运算并非是原子操作,导致volatile;变量在并发下一样是不安全的。比如下例:/** * volatile变量自增测试运算 * @author xpc * @date 2018年12月16日下午8:40:02 /public class VolatileTest { public static volatile int race=0; public static void increase() { race++; } public static final int THREADCOUNT=20; public static void main(String[] args) { Thread[] threads=new Thread[THREADCOUNT]; for(int i=0;i<THREADCOUNT;i++) { threads[i]=new Thread(()->{ for(int j=0;j<10000;j++) increase(); }); threads[i].start(); } while(Thread.activeCount()>1) { Thread.yield(); } System.out.println(race);//最后打印的结果是小于2010000即200000的数 }}其自增部分对应的字节码为 public static void increase(); descriptor: ()V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=0, args_size=0 0: getstatic #13 // Field race:I 3: iconst_1 4: iadd 5: putstatic #13 // Field race:I 8: return LineNumberTable: line 11: 0 line 12: 8 LocalVariableTable: Start Length Slot Name Signature  之所以最后输出的结果小于200000,并且每次运行程序输出的结果都不一样。问题就出现在自增运算race++上,反编译后发现一个race++会产生4条字节码指令(不包括return),从字节码层面很容易分析出并发失败的原因:当getstatic指令把race的值取到操作栈顶时,volatile关键字保证了race的值在此时是正确的,但在执行iconst_1、iadd这些指令的时候,其他线程可能已经把race的值加大了,而在操作栈顶的值就变成了过期的数据,所以putstatic指令执行后就可能把较小的race值同步回主内存之中。  这里使用字节码来分析并发问题仍然是不严谨的,因为即使编译出来只有一条字节码指令,也不意味这执行这条指令就是一个原子操作。一个字节码指令在解释执行时,解释器将要运行许多行代码才能实现它的语义。如果是编译执行,一条字节码指令可能转化为若干条本地机器码指令。  由于volatile只能保证可见性,在不符合以下两条规则的运算场景中,仍然要通过加锁(使用synchronized或者java.util.concurrent中的原子类)来保证原子性。同时满足以下两条规则的运算场景才适合使用volatile去保证原子性运算结果并不依赖变量的当前值,或者,能够确保只有单一线程修改变量的值变量不需要与其他的状态变量共同参与不变约束满足第一条但不满足第二条的一个例子:volatile static int start = 3;volatile static int end = 6;//只有线程B修改变量的值,满足了第一条。尽管不满足运算结果不依赖变量的当前值,false||ture==ture线程A执行如下代码:while (start < end){//do something}线程B执行如下代码:start+=3;end+=3;适合使用volatile来控制并发的场景的例子,当shutdown()方法被调用时,能保证所有线程中执行的doWork()方法都立即停止下来。volatile boolean shutdownRequested;public void shutdown(){ shutdownRequested=ture;}public void doWork(){ while(!shutdownRequested){ //do stuff }}禁止指令重排序指令重排序干扰程序的并发执行的例子Map configOptions;char[] configText;//此变量必须定义为volatilevolatile boolean initialized=false;//假设以下代码在线程A中执行//模拟读取配置信息,当读取完后将initialized设置为true以通知其他线程配置可用configOptions=new HashMap();configText=readConfigFile(fileName);processConfigOptions(configText,configOptions);initialized=true;//假设以下代码在线程B中执行//等待initialized为true,代表线程A已经把配置信息处理化完成while(!initialized){ sleep();}//使用线程A初始化好的配置信息doSomethingWithConfig();  在这个例子中,如果定义initialized变量时没有使用volatile修饰,就可能会由于指令重排序的优化,导致位于线程A中的最后一句的代码initialized=true;被提前执行(虽然使用java代码来作为伪代码,但所指的重排序优化是机器级的优化操作,提前执行是值这句话对应的汇编代码被提前执行),这样在线程B中使用配置信息的代码就可能出现错误,而volatile关键字则可以避免此类情况的发生。