由CopyOnWriteArrayList类的set方法引发对volatile深入理解

转载自:http://ifeve.com/copyonwritearraylist-set/
              http://ifeve.com/java-memory-model-4/
              http://tech.meituan.com/java-memory-reordering.html

       在CopyOnWriteArrayList类的set方法中有一段setArray(elements)代码(else块),实际上这段代码并未对elements做任何改动,注意这里的,实现的volatile语意并不对CopyOnWriteArrayList实例产生任何影响,为什么还是要保留这行语句?

[java] view plain copy
  1. private transient volatile Object[] array;  
  2.   
  3. final Object[] getArray() {  
  4.     return array;  
  5. }  
  6.   
  7. final void setArray(Object[] a) {  
  8.     array = a;  
  9. }  
  10.   
  11. public E set(int index, E element) {  
  12.     final ReentrantLock lock = this.lock;  
  13.     lock.lock();  
  14.     try {  
  15.         Object[] elements = getArray();  
  16.         E oldValue = get(elements, index);  
  17.   
  18.         if (oldValue != element) {  
  19.             int len = elements.length;  
  20.             Object[] newElements = Arrays.copyOf(elements, len);  
  21.             newElements[index] = element;  
  22.             setArray(newElements);  
  23.         } else {  
  24.             // Not quite a no-op; ensures volatile write semantics  
  25.             setArray(elements);  
  26.         }  
  27.         return oldValue;  
  28.     } finally {  
  29.         lock.unlock();  
  30.     }  
  31. }  
       在这里首先理解下volatile,我们知道volatile变量自身具有下列特性:
       可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
       原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。

       从JSR-133开始,volatile变量的写-读可以实现线程之间的通信。从内存语义的角度来说,volatile与锁有相同的效果。
       下面使用volatile变量的示例代码:
[java] view plain copy
  1. class VolatileExample {  
  2.     int a = 0;  
  3.     volatile boolean flag = false;  
  4.   
  5.     public void writer() {  
  6.         a = 1;                   //1  
  7.         flag = true;               //2  
  8.     }  
  9.   
  10.     public void reader() {  
  11.         if (flag) {                //3  
  12.             int i =  a;           //4  
  13.             ……  
  14.         }  
  15.     }  
  16. }  
       假设线程A执行writer()方法之后,线程B执行reader()方法。根据happens before规则,这个过程建立的happens before关系可以分为两类:
       1. 根据程序次序规则,1 happens before 2; 3 happens before 4。
       2. 根据volatile规则,2 happens before 3。
       3. 根据happens before 的传递性规则,1 happens before 4。

volatile写-读的内存语义:
       volatile写的内存语义:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存。上面示例程序VolatileExample为例,假设线程A首先执行writer()方法,随后线程B执行reader()方法,初始时两个线程的本地内存中的flag和a都是初始状态。下图是线程A执行volatile写后,共享变量的状态示意图:
      
       如上图所示,线程A在写flag变量后,本地内存A中被线程A更新过的两个共享变量的值被刷新到主内存中。此时,本地内存A和主内存中的共享变量的值是一致的。

volatile读的内存语义
       当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
       下面是线程B读同一个volatile变量后,共享变量的状态示意图:
            
       如上图所示,在读flag变量后,本地内存B已经被置为无效。此时,线程B必须从主内存中读取共享变量。线程B的读取操作将导致本地内存B与主内存中的共享变量的值也变成一致的了。
       如果把volatile写和volatile读这两个步骤综合起来看的话,在读线程B读一个volatile变量后,写线程A在写这个volatile变量之前所有可见的共享变量的值都将立即变得对读线程B可见。

下面对volatile写和volatile读的内存语义做个总结:
       线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所在修改的)消息。
       线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息。
       线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。

那么JMM如何实现volatile写/读的内存语义?
      前文提到过重排序分为编译器重排序和处理器重排序,为了实现volatile内存语义,JMM会分别限制这两种类型的重排序类型。下面是JMM针对编译器制定的volatile重排序规则表:
      
       举例来说,在程序顺序中,当第一个操作为普通变量的读或写时,如果第二个操作为volatile写,则编译器不能重排序这两个操作。
       从上表可以看出:
        • 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
        • 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
        • 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
       为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

       内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题。Java编译器也会根据内存屏障的规则禁止重排序。
       内存屏障可以被分为以下几种类型:
          LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
          StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
          LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
          StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。

       JSR-133的规定,Java编译器会这样使用内存屏障。下面是JMM内存屏障插入策略:
        • 在每个volatile写操作的前面插入一个StoreStore屏障。
        • 在每个volatile写操作的后面插入一个StoreLoad屏障。
        • 在每个volatile读操作的后面插入一个LoadLoad屏障。
        • 在每个volatile读操作的后面插入一个LoadStore屏障。
        • 为了保证final字段的特殊语义,也会在下面的语句加入内存屏障。x.finalField = v; StoreStore; sharedRef = x;
       上述内存屏障插入策略非常保守,但它可以保证在任意处理器平台,任意的程序中都能得到正确的volatile内存语义。关于JMM主内存与工作内存交互,可以参考:http://blog.csdn.net/zero__007/article/details/53025425
       下面是保守策略下,volatile写插入内存屏障后生成的指令序列示意图:
       
       上图中的StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作已经对任意处理器可见了。这是因为StoreStore屏障将保障上面所有的普通写在volatile写之前刷新到主内存。volatile写后面的StoreLoad屏障的作用是避免volatile写与后面可能有的volatile读/写操作重排序。因为编译器常常无法准确判断在一个volatile写的后面,是否需要插入一个StoreLoad屏障(比如,一个volatile写之后方法立即return)。
       为了保证能正确实现volatile的内存语义,JMM在这里采取了保守策略:在每个volatile写的后面或在每个volatile读的前面插入一个StoreLoad屏障。从整体执行效率的角度考虑,JMM选择了在每个volatile写的后面插入一个StoreLoad屏障。因为volatile写-读内存语义的常见使用模式是:一个写线程写volatile变量,多个读线程读同一个volatile 变量。当读线程的数量大大超过写线程时,选择在volatile写之后插入StoreLoad屏障将带来可观的执行效率的提升。
       下面是在保守策略下,volatile读插入内存屏障后生成的指令序列示意图:
       
       上图中的LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。
       上述volatile写和volatile读的内存屏障插入策略非常保守。在实际执行时,只要不改变volatile写-读的内存语义,编译器可以根据具体情况省略不必要的屏障。下面通过具体的示例代码来说明:
[java] view plain copy
  1. class VolatileBarrierExample {  
  2.     int a;  
  3.     volatile int v1 = 1;  
  4.     volatile int v2 = 2;  
  5.   
  6.     void readAndWrite() {  
  7.         int i = v1;           //第一个volatile读  
  8.         int j = v2;           // 第二个volatile读  
  9.         a = i + j;            //普通写  
  10.         v1 = i + 1;          // 第一个volatile写  
  11.         v2 = j * 2;          //第二个 volatile写  
  12.     }  
  13.   
  14.     …                    //其他方法  
  15. }  
       针对readAndWrite()方法,编译器在生成字节码时可以做如下的优化:
       
       注意,最后的StoreLoad屏障不能省略。因为第二个volatile写之后,方法立即return。此时编译器可能无法准确断定后面是否会有volatile读或写,为了安全起见,编译器常常会在这里插入一个StoreLoad屏障。

       在JSR-133之前的旧Java内存模型中,虽然不允许volatile变量之间重排序,但旧的Java内存模型允许volatile变量与普通变量之间重排序。在旧的内存模型中,VolatileExample示例程序可能被重排序成下列时序来执行:
       
       在旧的内存模型中,当1和2之间没有数据依赖关系时,1和2之间就可能被重排序(3和4类似)。其结果就是:读线程B执行4时,不一定能看到写线程A在执行1时对共享变量的修改。
       因此在旧的内存模型中 ,volatile的写-读没有锁的释放-获所具有的内存语义。为了提供一种比锁更轻量级的线程之间通信的机制,JSR-133增强 volatile的内存语义:严格限制编译器和处理器对volatile变量与普通变量的重排序,确保volatile的写-读和锁的释放-获取一样,具有相同的内存语义。从编译器重排序规则和处理器内存屏障插入策略来看,只要volatile变量与普通变量之间的重排序可能会破坏volatile的内存语意,这种重排序就会被编译器重排序规则和处理器内存屏障插入策略禁止。


       那么回到之前的问题,未对elements做任何改动为什还要在else块中调用setArray(elements);是否是多余呢?
       如下例子:a为非volatile的某基本类型变量,coal为CopyOnWriteArrayList对象,
[java] view plain copy
  1. thread1:  
  2. x:a = calValue;  
  3. y:coal.set….  
  4. ———————  
  5. thread2:  
  6. m:coal.get…  
  7. n:int tmp = a;  
       假设存在以上场景,如果能保证只会存在这样的轨迹:x,y,m,n.根据约定有happen-before(y,m),根据线程内的操作相关规定有happen-before(x,y), happen-before(m,n),(volitile语义)根据happen-before的传递性读写a变量就有happen-before(x,n),所以thread1对a的写操作对thread2中a的读操作可见。如果CopyOnWriteArrayList的set的else里没有setArray(elements) 的话,before(y,m)就不再有了,上述的可见性也就无法保证。
       所以实际上else中的setArray(elements)不是为了保证CopyOnWriteArrayList本身的可见性,而是保证外部的非volatile变量的happen-before。可以参考http://stackoverflow.com/questions/28772539/why-setarray-method-call-required-in-copyonwritearraylist。

相关阅读:http://blog.csdn.net/zero__007/article/details/44080975
原文地址:https://www.cnblogs.com/silyvin/p/9106641.html