【并发】volatile是否能保证数组中元素的可见性?

 

问题

 

一个线程向volatile的数组中设置值,而另一个线程向volatile的数组中读取。
比如seg.setValue(2),随后另一个线程调用seg.getValue(2),前一个线程设置的值对读取的线程是可见的吗?

我看书上说volatile的数组只针对数组的引用具有volatile的语义,而不是它的元素。

 

对一个共享变量使用Volatile关键字保证了线程间对该数据的可见性,即不会读到脏数据。

注:1. 可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入

        2. 原子性:对任意单个volatile变量的读/写具有原子性(long,double这2个8字节的除外),但类似于volatile++这种复合操作不具有原子性。

        3. volatile修饰的变量如果是对象或数组之类的,其含义是对象获数组的地址具有可见性,但是数组或对象内部的成员改变不具备可见性

            eg:以下代码要以-server模式运行,强制虚拟机开启优化

[java] view plain copy

  1. package com.xj;  
  2.   
  3. public class VolatileObjectTest implements Runnable {  
  4.     private ObjectA a; // 加上volatile 就可以正常结束While循环了     
  5.     public VolatileObjectTest(ObjectA a) {  
  6.         this.a = a;  
  7.     }  
  8.    
  9.     public ObjectA getA() {  
  10.         return a;  
  11.     }  
  12.    
  13.     public void setA(ObjectA a) {  
  14.         this.a = a;  
  15.     }  
  16.    
  17.     @Override  
  18.     public void run() {  
  19.         long i = 0;  
  20.         while (a.isFlag()) {  
  21.             i++;  
  22. //            System.out.println("------------------");  
  23.         }  
  24.         System.out.println("stop My Thread " + i);  
  25.     }  
  26.    
  27.     public void stop() {  
  28.         a.setFlag(false);  
  29.     }  
  30.    
  31.     public static void main(String[] args) throws InterruptedException {  
  32.          // 如果启动的时候加上-server 参数则会 输出 Java HotSpot(TM) Server VM  
  33.         System.out.println(System.getProperty("java.vm.name"));  
  34.            
  35.         VolatileObjectTest test = new VolatileObjectTest(new ObjectA());  
  36.         new Thread(test).start();  
  37.    
  38.         Thread.sleep(1000);  
  39.         test.stop();  
  40.         Thread.sleep(1000);  
  41.         System.out.println("Main Thread " + test.getA().isFlag());  
  42.     }  
  43.    
  44.     static class ObjectA {  
  45.         private boolean flag = true;  
  46.    
  47.         public boolean isFlag() {  
  48.             return flag;  
  49.         }  
  50.    
  51.         public void setFlag(boolean flag) {  
  52.             this.flag = flag;  
  53.         }  
  54.    
  55.     }  
  56. }  

       以上代码如果是红色标记那一行加volatile关键字,子线程是可以退出循环的,不加的话,子线程没法退出循环,如此说来,volatile变量修饰对象或者数组,当我们改变对象或者数组的成员的时候,岂非不同线程之间具有可见性?

      在看如下代码:

[java] view plain copy

  1. package com.xj;  
  2.   
  3. public class VolatileTestAgain implements Runnable {  
  4.     private "color:#ff0000;">volatile ObjectA a; // 加上volatile也无法结束While循环了  
  5.    
  6.     public VolatileTestAgain(ObjectA a) {  
  7.         this.a = a;  
  8.     }  
  9.    
  10.     public ObjectA getA() {  
  11.         return a;  
  12.     }  
  13.    
  14.     public void setA(ObjectA a) {  
  15.         this.a = a;  
  16.     }  
  17.    
  18.     @Override  
  19.     public void run() {  
  20.         long i = 0;  
  21.         ObjectASub sub = a.getSub();  
  22.         while (!sub.isFlag()) {  
  23.             i++;        }  
  24.         System.out.println("stop My Thread " + i);  
  25.     }  
  26.     
  27.     public static void main(String[] args) throws InterruptedException {  
  28.          // 如果启动的时候加上-server 参数则会 输出 Java HotSpot(TM) Server VM  
  29.         System.out.println(System.getProperty("java.vm.name"));  
  30.         ObjectASub "color:#ff0000;">sub = new ObjectASub();  
  31.         ObjectA sa = new ObjectA();  
  32.         sa.setSub(sub);  
  33.         VolatileTestAgain test = new VolatileTestAgain(sa);  
  34.         new Thread(test).start();  
  35.    
  36.         Thread.sleep(1000);  
  37.         sub.setFlag(true);  
  38.         Thread.sleep(1000);  
  39.         System.out.println("Main Thread " + sub.isFlag());  
  40.     }  
  41.    
  42.     static class ObjectA {  
  43.         private ObjectASub sub;  
  44.   
  45.         public ObjectASub getSub() {  
  46.             return sub;  
  47.         }  
  48.   
  49.         public void setSub(ObjectASub sub) {  
  50.             this.sub = sub;  
  51.         }  
  52.     }  
  53.       
  54.     static class ObjectASub{  
  55.         private boolean flag;  
  56.   
  57.         public boolean isFlag() {  
  58.             return flag;  
  59.         }  
  60.   
  61.         public void setFlag(boolean flag) {  
  62.             this.flag = flag;  
  63.         }  
  64.           
  65.           
  66.     }  
  67. }  

      如上代码即使添加volatile关键字也无法让子线程结束循环,读者可以仔细对比一下2段代码,下面是我的解释。

     1)代码1中当主线程更改flag字段时候,是调用stop()方法里面的“a.setFlag(false); ”,注意这一句其实包含多步操作,含义丰富:首先是对volatile变量a的读,既然是volatile变量,当然读到的是主存(而不是线程私有的)中的地址,然后再setFlag来更新这个标志位实际上是更新的主存中引用指向的堆内存;然后是子线程读

a.isFlag(),同样的包含多步:首先是对volatile变量的读,当然读的是主存中的引用地址,然后根据这个引用找堆内存中flag值,当然找到的就是之前主线程写进去的值,所以能够立即生效,子线程退出。

    2)代码1中虽然主线程和子线程都是读volatile值,然后一个是改,一个是读,按照java内存模型中的happen-before,2个线程对volatile变量的读是不具有happen-before特性的,但是这里要注意的是,因为都是以volatile变量为根,层层引用,最后找到的都是同一块堆内存,然后一个修改,一个查看,所以实际上相当于同一个线程在写和修改(因为写和修改的是同一块内存);所以可以利用happen-before中第一条规则——程序顺序规则,从而有主线程的写happen-before子线程的读

    3)代码2中加了volatile关键字仍然子线程无法退出,这是因为主线程的对flag标志位的改,已经不是通过volatile根对象先定位到主存中的地址,然后逐级索引去找到堆内存,然后改地址,而是直接在线程中保存了一个sub对象,这样改掉的,实际上不是主存中的volatile根对象引用的ObjectASub对象再引用的flag标志位的值了,他改变的是本地线程中缓存的值;同理子线程中取的也是每次都取的本地线程中缓存的值;主线程的写没有及时刷新到主存中,子线程也没用从主存中去读,导致了数据的不一致性。

    总结:1)用volatile修饰数组和对象不是不可以,要注意一点:修改操作要从volatile变量逐级引用,去找到要修改的变量,保证修改是刷新到主存中的值对应的变量;读取操作,也要以volatile变量为根,逐级去定位,这样保证修改即使刷新到主存中volatile变量指向的堆内存,读取能够每次从主存的volatile变量指向的堆内存去读,保证数据的一致性。

                2)在保证了总结1)的前提下,因为大家读取修改的都是同一块内存,所以变相的符合happen-before规则中的程序顺序规则,具有happen-before性。

      3. volatile写-读建立的happens before关系

              对程序员来说,volatile对线程的内存可见性的影响比volatile自身的特性更为重要,也更需要我们去关注.

             happen-before规则:

             程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作。 
             监视器锁规则:对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁。 
             volatile变量规则:对一个volatile域的写,happens- before 于任意后续对这个volatile域的读。
            传递性:如果A happens- before B,且B happens- before C,那么A happens- before C。
             Thread.start()的调用会happens-before于启动线程里面的动作。 
             Thread中的所有动作都happens-before于其他线程从Thread.join中成功返回。

            

             进一步关注JMM如何实现volatile写/读的内存语义

            前文我们提到过重排序分为编译器重排序和处理器重排序。为了实现volatile内存语义,JMM会分别限制这两种类型的重排序类型。下面是JMM针对编译器制定的volatile

            重排序规则表:

 

是否能重排序 第二个操作
第一个操作 普通读/写 volatile读 volatile写
普通读/写     NO
volatile读 NO NO NO
volatile写   NO NO

 

举例来说,第三行最后一个单元格的意思是:在程序顺序中,当第一个操作为普通变量的读或写时,如果第二个操作为volatile写,则编译器不能重排序这两个操作。


从上表我们可以看出:
当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。

当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。

     eg:(对以上volatile的happen-before特性的利用)

java并发库ConcurrentHashMap中get操作的无锁弱一致性实现

[java] view plain copy

  1. V get(Object key, int hash) {     
  2.     "color:#ff0000;">if (count != 0) { // read-volatile     
  3.         HashEntry e = getFirst(hash);     
  4.         while (e != null) {     
  5.             if (e.hash == hash && key.equals(e.key)) {     
  6.                 V v = e.value;     
  7.                 if (v != null)     
  8.                     return v;     
  9.                 return readValueUnderLock(e); // recheck     
  10.             }     
  11.             e = e.next;     
  12.         }     
  13.     }     
  14.     return null;     
  15. }    

[java] view plain copy

  1. V put(K key, int hash, V value, boolean onlyIfAbsent) {  
  2.            lock();  
  3.            try {  
  4.                int c = count;  
  5.                if (c++ > threshold) // ensure capacity  
  6.                    rehash();  
  7.                HashEntry[] tab = table;  
  8.                int index = hash & (tab.length - 1);  
  9.                HashEntry first = tab[index];  
  10.                HashEntry e = first;  
  11.                while (e != null && (e.hash != hash || !key.equals(e.key)))  
  12.                    e = e.next;  
  13.   
  14.                V oldValue;  
  15.                if (e != null) {  
  16.                    oldValue = e.value;  
  17.                    if (!onlyIfAbsent)  
  18.                        e.value = value;  
  19.                }  
  20.                else {  
  21.                    oldValue = null;  
  22.                    ++modCount;  
  23.                    tab[index] = new HashEntry(key, hash, first, value);  
  24.                    "color:#ff0000;">count = c; // write-volatile  
  25. lt;/span>                }  
  26.                return oldValue;  
  27.            } finally {  
  28.                unlock();  
  29.            }  
  30.        }  

get操作不需要锁。第一步是访问count变量,这是一个volatile变量,由于所有的修改操作在进行结构修改时都会在最后一步写count变量,通过这种机制保证get操作能够得到几乎最新的结构更新。对于非结构更新,也就是结点值的改变,由于HashEntry的value变量是volatile的,也能保证读取到最新的值。接下来就是对hash链进行遍历找到要获取的结点,如果没有找到,直接访回null。对hash链进行遍历不需要加锁的原因在于链指针next是final的。但是头指针却不是final的,这是通过getFirst(hash)方法返回,也就是存在table数组中的值。这使得getFirst(hash)可能返回过时的头结点,例如,当执行get方法时,刚执行完getFirst(hash)之后,另一个线程执行了删除操作并更新头结点,这就导致get方法中返回的头结点不是最新的。这是可以允许,通过对count变量的协调机制,get能读取到几乎最新的数据,虽然可能不是最新的。要得到最新的数据,只有采用完全的同步。

欢迎关注公众号:

【并发】volatile是否能保证数组中元素的可见性?_第1张图片

你可能感兴趣的:(Java核心技术分析,Java核心技术分析)