Java并发之(1):volatile关键字(TIJ21-21.3.3 21.3.4)

Java并发Java服务器端编程的一项必备技能。

** 1 简介
    volatile是java中的一个保留关键字,它在英语中的含义是易变的,不稳定的。volatile像final、static等其他修饰符 一样,可以修饰class中的域,而不能修饰方法中的局部变量。当修饰class中的域时,volatile可以修饰primative类型或者任意对 象。下面这个例子展示了这一点:

1 public class TIJ_volatile {
2     private volatile int i;
3     private volatile String s;
4     private volatile Object o = new Object();
5     private volatile Object o2;
6     public static void main(String... args) {
7         volatile int i2 = 1;
8     }
9 }

编译器会在main方法的volatile int i2 = 1 这一行给出错误警报。


** 2 一个例子看懂volatile域与普通域的区别


    那么,声明为volatile的变量与普通域变量有什么区别呢?从字面意思理解,相对于那么未声明为volatile的变量,volatile变量是更加不稳定的,更加易变的。
一个典型的需要不同的线程读写简单变量的例子,是允许一个线程通知另一个线程终止的“停止请求”标志位,在这个例子中volatile是非常好的选择:

 1 public class StoppableTask extends Thread {
 2   private volatile boolean pleaseStop;  //pleaseStop变量存储在java堆中, 在new StoppableTask的时候。
 3   public void run() {
 4     while (!pleaseStop) {
 5       // do some stuff...
 6     }
 7   }
 8 
 9   public void tellMeToStop() {
10     pleaseStop = true;
11   }
12 }


    在上面的例子中,如果这个“停止请求”标志位没有被声明为volatile,同时又没有使用其他的同步机制,那么StoppableTask线程会从主内存中读取一次pleaseStop变量,并把它缓存在自己的线程栈(工作内存)中(在一些文章中,工作内存被看做是jvm对物理机器中的寄存器或者高速缓存的一种虚拟或者抽象,因此它的读写速度远高于主内存,从工作内存中读取数据可以提高读取速度,优化性能)。在后面的循环中,pleaseStop将不会被再次从主内存中读取,因此while循环将永远执行下去,程序失败。这就是非volatile变量的读取和写入方式。 (在Object.wait()的使用时常使用while(flag==true)进行判断,而且flag不是volatile的,但锁可以实现比volatile更强的Happen-Before关系)。

    当声明一个变量为volatile变量时,就等于告诉编译器,这个变量的值是非常易变的,所以不能像普通的变量那样,为了优化性能,对它的读取都从工作内存中进行,而不与主内存同步:因为如果不同步的话,volatile的值又是“易变的”,那么读取的工作内存中缓存的值有可能不是最新的,由此可能引发程序的执行失败!因为volatile值的工作内存中的缓存值可以与主内存的最新值及时同步,所以它的值永远看起来就像是直接从主内存中读取的,也永远像是直接往主内存中写入的。

    相信到这里,大家应该明白了volatile的含义了吧?需要特别注意的是,volatile的这种语义直到java 5才得以完全正确地实现。在jva5以前的版本中,volatile并不能正常地工作。 在java5及其后续版本中,对volatile变量的读取看起来就像是直接操作主内存一样:它使得所有缓存在线程工作内存中的该变量的拷贝可以有效地与主内存中的变量同步。(volatile变量在不同线程中仍然有不同的拷贝)。

 

** 3 java内存模型(jmm)与可见性

    那么什么是主内存(main memory)呢?什么又是工作内存呢? 这就不可避免地涉及到了java内存模型(jmm,java memory model)。jmm将会在本博客中专门用一篇文章来阐述,在这里我们只需要看一下下面的这幅流传深远的jmm示意图:

                            

    从上图可以看出,在jvm中,每个线程都有一个自己的工作内存(Working Memory),线程总是对主内存(Main Memory)中的变量进行备份,并存储在自己的工作内存中,并且对变量的读写都是在工作内存中进行的(图中的Load/Save过程)。正常情况下,线程在第一次从主存中读取该变量后,就假设该变量是相对稳定的、不易变的,因此当该线程以后再用到该变量,就假设自己工作内存中的备份与主内存中的变量是一致的,因此它会直接读取自己工作内存中的备份,这样做的主要目的是提高性能,因为从内存读取数据相对于高速缓存读取甚至是寄存器读取,是要慢很多的。

    volatile变量的作用,就是要告诉线程:该变量是不稳定的,非常容易变化,因此每次读取工作内存中的该数据的备份,都要从主内存中重新load一遍。注意这样做相对于普通变量而言,会降低性能。讲到这里,其实可见性的含义也已经出来了(下面这段来源于网络):

    相对于内存,CPU的速度是极高的,如果CPU需要存取数据时都直接与内存打交道,在存取过程中,CPU将一直空闲,这是一种 极大的浪费,所以,现代的CPU里都有很多寄存器,多级cache,他们比内存的存取速度高多了。某个线程执行时,内存中的一份数 据,会存在于该线程的工作存储中(working memory,是cache和寄存器的一个抽象,这个解释源于《Concurrent Programming in Java: Design Principles and Patterns, Second Edition》§2.2.7,原文:Every thread is defined to have a working memory (an abstraction of caches and registers) in which to store values. 有不少人觉得working memory是内存的某个部分,这可能是有些译作将working memory译为工作内存的缘故,为避免混淆,这里称其为工作存储,每个线程都有自己的工作存储),并在某个特定时候回写到内存。单线程时,这没有问题, 如果是多线程要同时访问同一个变量呢?内存中一个变量会存在于多个工作存储中,线程1修改了变量a的值什么时候对线程2可见?此外,编译器或运行时为了效 率可以在允许的时候对指令进行重排序,重排序后的执行顺序就与代码不一致了,这样线程2读取某个变量的时候线程1可能还没有进行写入操作呢,虽然代码顺序 上写操作是在前面的。

    根据上述描述我们不难看出,可见性其实指的是某一线程对主内存中的数据的修改,对其他线程而言是不是可见。

** 4 与synchronization等锁机制的区别

    volatile关键字仅提供了synchronization提供的部分同步功能,也就是说,volatile保证了主内存中数据对所有线程的可见性,或者说提供了主内存与工作内存之间的同步功能。 但是,相对于synchronized关键字在java server端编程中的广泛应用,volatile的使用率相对来说是比较低的,因为它并不能解决所有可以由synchronized关键字解决的同步问题。

    从下面这个例子可以看出,volatile并不能保证线程同时写变量所带来的同步问题:

 1 public class Counter {
 2      public volatile static int count = 0;
 3      public static void inc() {
 4          //这里延迟1毫秒,使得结果明显
 5         try {
 6             Thread.sleep(1);
 7         } catch (InterruptedException e) {
 8         }
 9          count++; // 在java中不是原子操作。可以使用AtomicInteger保证原子性。
10     }
11      public static void main(String[] args) {
12          //同时启动1000个线程,去进行i++计算,看看实际结果
13          for (int i = 0; i < 1000; i++) {
14             new Thread(new Runnable() {
15                 @Override
16                 public void run() {
17                     Counter.inc();
18                 }
19             }).start();
20         }
21          //这里每次运行的值都有可能不同,可能为1000
22         System.out.println("运行结果:Counter.count=" + Counter.count);
23     }
24 }

 

     上述程序在多线程环境下,大多时候会失败。 究其原因,程序中对count变量使用了volatile修饰符,保证了不同线程读取count的值时(在工作内存中读取),count值都与主内存中的值是一致的。但是这种一致性并不能保证两个线程同时执行count++时,count的值只增加了一次。为什么会这样的,原因在于在java中,count++操作并不是原子操作(注意在c++中count++是原子操作)。 count++本质上是以下原子操作的复合体: 从主内存中读取(load) count的值到线程的工作内存,在工作内存中对count的副本执行count++操作,把工作内存中的count值(更新后的)写入(save)到主内存。

    在两个线程同时执行count++操作时,有可能出现以下情况:

    假设主内存中count的当前值为0, thread 1 读取该值到工作内存中(0),此时线程调度机制中断了线程1,thread 2开始执行并读取该值到它的工作内存(0),此时线程调度机制再次中断了thread 2,thread1重新执行,并执行count++操作(针对工作内存中的count副本),此时即使重新从主内存中同步count,该值仍然为0,所以工作内存中的count值在执行count++操作后为1.此时线程调度机制再次中断thread1,thread2重新执行,并执行count++操作,因为此时thread1工作内存中的count值(1)并未save到主内存,所以thread2工作内存中的count副本在执行count++操作后,值更新为1.

    在后续的程序执行中,不论thread1 thread2以何种顺序执行,最终主内存中的count值都为1,程序执行失败。

    为了让该程序正确执行,有两种修改的方法,第一种是通过synchronized关键字使用内置锁(或者通过lock对象使用显式锁);第二种方法是使用在java5中引入的Atomic原子类。(原子类会在本文后面介绍到)。


** 5 原子性与原子类


    在上一节的例子中,已经涉及到了原子性的概念。原子性可以保证操作未完成前,不会被线程调度机制中断。也就是说,原子操作是保证在该操作完成前,不会被线程调度机制中断的操作。 需要注意的是原子性并不能保证并发程序的正确性:

 

 1 public class TIJ_21_3_3_atomicityTest implements Runnable {
 2 
 3    private int i = 0;
 4 
 5    public int getValue() { return i; }
 6 
 7    private synchronized void evenIncrement() { i++; i++; } // evenIncrement不是原子性的,可以被线程调度机制中断
 8 
 9    public void run() {
10 
11      while(true) evenIncrement();
12 
13    }
14 
15    public static void main(String[] args) {
16 
17      ExecutorService exec = Executors.newCachedThreadPool();
18 
19      TIJ_21_3_3_atomicityTest at = new TIJ_21_3_3_atomicityTest();
20 
21      exec.execute(at);
22 
23      while(true) {
24 
25        int val = at.getValue();
26 
27        if(val % 2 != 0) {
28 
29          System.out.println(val);
30 
31          System.exit(0);
32 
33        }
34 
35      }
36 
37    }
38 
39  } /* Output: (Sample)
40 
41  191583767
42 
43  *///:~

 

    在上面的例子中,虽然getValue()方法中的操作是原子操作,但这并不能保证程序的正确性。可见原子性跟正确的并发是两个完全不同的概念。

    原子性介绍完了,那么,原子类又是什么呢?下面来看一下: 原子类的概念是在Java SE5中引入的。典型的原子类包括:Atomiclnteger, AtomicLong, AtomicReference,等. 它们提供了以下形式的原子性条件更新操作: boolean compareAndSet(expectedValue, updateValue);以及原子性的修改操作:

 int addAndSet(int delta)。看下面的例子:

 1 //: concurrency/AtomicIntegerTest.java
 2 import java.util.concurrent.*;
 3 import java.util.concurrent.atomic.*;
 4 import java.util.*;
 5 
 6 public class AtomicIntegerTest implements Runnable {
 7   private AtomicInteger i = new AtomicInteger(0);
 8   public int getValue() { return i.get(); }
 9   private void evenIncrement() { i.addAndGet(2); } // 不能被中断的原子操作
10   public void run() {
11     while(true)
12       evenIncrement();
13   }
14   public static void main(String[] args) {
15     new Timer().schedule(new TimerTask() {
16       public void run() {
17         System.err.println("Aborting");
18         System.exit(0);
19       }
20     }, 5000); // Terminate after 5 seconds
21     ExecutorService exec = Executors.newCachedThreadPool();
22     AtomicIntegerTest ait = new AtomicIntegerTest();
23     exec.execute(ait);
24     while(true) {
25       int val = ait.getValue();
26       if(val % 2 != 0) {
27         System.out.println(val);
28         System.exit(0);
29       }
30     }
31 
32 }
33 } ///:~

 

    在上面的例子中,我们使用AtomicInteger来代替synchronized关键字。不过需要强调的是,原子类被设计用来实现java.util.concurrent中的类,只有在少数情况下才需要在自己的代码中使用它们。一般而言直接使用锁(synchronize或者显式锁对象)更加安全。

  
** 6 总结:什么时候使用volatile?

    volatile变量具备对某个对象的同步的读写的特性,尤其是内存同步特性。volatile不必要也不可能修饰final变量,因为final变量是不可改变的,而volatile的含义是易变的。当你对变量执行某些复杂操作,而又希望在执行这些操作的同时阻止其他线程对这些变量的访问时,你应该使用锁机制(synchronization或者是显式锁),而不是volatile。
    使用volatile的典型场景: 在一个线程中修改一个作为标志的变量,在另外一个线程中检查该标志变量; 最重要的是, 要写入的新值不依赖当前值。 上述原则使得诸如 x++ 或者是 x += 7之类的操作不适合使用volatile关键字,因为这类操作在java中并不是原子操作(在c c++中是原子操作),而是包含了读取当前值,增加,然后写入新值这样一系列的操作。一个普遍的误解是认为x++在java中是原子的。如果你需要这种原子性,则需要AtomicInteger或者相似的其他类。

    下面这个例子结合使用 volatile 和 synchronized 实现 “开销较低的读-写锁” :

1 public class CheesyCounter {
2     private volatile int value;
3     public int getValue() { return value; }
4     public synchronized int increment() {
5         return value++;
6     }
7 }

 

    increment函数是同步的,所以不能并发执行,保证了多线程写操作的正确性。在写操作的同时,多个线程可以同时读value变量,在同一时间不同线程读取的值可以保证是一致的,并且可以跟写操作并发执行。  

    最后,留一个问题:是不是跟Java的readLock& writeLock很像? Java的读写锁是如何实现的呢? 

参考文献:

http://www.cnblogs.com/aigongsi/archive/2012/04/01/2429166.html
http://javarevisited.blogspot.com/2011/06/volatile-keyword-java-example-tutorial.html
http://www.ibm.com/developerworks/cn/java/j-jtp06197.html
http://stackoverflow.com/questions/16719004/should-i-use-volatile-if-i-use-synchronized
 happens-before俗解 http://ifeve.com/easy-happens-before/
http://javamex.com/tutorials/synchronization_volatile_typical_use.shtml
http://javamex.com/tutorials/synchronization_concurrency_7_atomic_updaters.shtml
http://javamex.com/tutorials/synchronization_concurrency_7_atomic.shtml
http://javamex.com/tutorials/synchronization_concurrency_7_atomic_updaters.shtml
http://javamex.com/tutorials/synchronization_volatile_dangers.shtml

posted @ 2015-12-16 22:25  Zhao_Gang  阅读(328)  评论(0编辑  收藏  举报