Jvm

volatile变量

释放双眼,带上耳机,听听看~!

关键字volatile是虚拟机提供的一种轻量级的同步机制,但在做多线程开发时,大部分的人都不太习惯喜欢用volatile,而是直接用关键字synchronized来进行同步。但有些时候在解决线程安全问题时,使用volatile关键字要比使用synchronized同步函数更好一些。下面我们详细分析一下在Java中关键字volatile到底是一个什么样的东东,底层是怎么实现的。

在JMM中虚拟机对volatile关键字专门定义了一些特殊的访问规则,来实现线程的轻量级同步。当变量是一个volatile变量时,它就会具有两种特殊的特性。

  • 第一个特性就是保证此变量对所有线程的可见性。可见性的意思是说当一个线程修改了volatile变量时,对于其它线程可以立即知道修改后的值。而普通变量则不具有此特性,普通变量只能通过主内存来完成。也就是说如果某一个线程修改了普通变量的值,只能向主内存同步后,其它线程在从主内存中拷贝后,才可以得到新的普通变量的值。虽然volatile变理具有可见性,但在使用volatile变量时,并不能保证不会出现线程安全问题。我们请看下面的代码。
public class VolatileTest {

    private static ExecutorService executorService = Executors.newFixedThreadPool(10);

    private static CountDownLatch countDownLatch = new CountDownLatch(10);

    public static volatile int race = 0;

    public static void increase() {
        race++;
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 1000; i++) {
                        increase();
                    }
                    countDownLatch.countDown();
                }
            });
        }
        countDownLatch.await();
        System.out.println(race);
    }
}
9149

按照代码的逻辑,每个线程都会调用1000次increase()方法,也就是每一个线程都会执行1000次volatile变量的自增。并且我们开启了10个线程,按照volatile变量可见性的分析,在正常情况下最后输出的值就应该是10000。但实际的结果却小于10000这是为什么呢?

问题的原因就是虽然volatile变量具有可见性,能够保证其它线程立即获取到更新后的值。但问题的原因却是自增运算导致的。也就是race++。在Java中自增运算并不是原子性的操作,所以在程序运行自增运算时,其它线程也有可能执行。我们使用javap命令来查看一下编译后的字节码就明白了其中的缘由了。

Classfile /C:/Java/jdk1.8.0_91/bin/VolatileTest.class
  Last modified 2017-4-20; size 1283 bytes
  MD5 checksum e30b4b72f4f6107ac01aa3953fa8bbe9
  Compiled from "VolatileTest.java"
public class VolatileTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Fieldref           #14.#38        // VolatileTest.countDownLatch:Ljava/util/concurrent/CountDownLatch;
   #2 = Methodref          #15.#39        // java/lang/Object."<init>":()V
   #3 = Fieldref           #14.#40        // VolatileTest.race:I
   #4 = Fieldref           #14.#41        // VolatileTest.executorService:Ljava/util/concurrent/ExecutorService;
   #5 = Class              #42            // VolatileTest$1
   #6 = Methodref          #5.#39         // VolatileTest$1."<init>":()V
   #7 = InterfaceMethodref #43.#44        // java/util/concurrent/ExecutorService.execute:(Ljava/lang/Runnable;)V
   #8 = Methodref          #12.#45        // java/util/concurrent/CountDownLatch.await:()V
   #9 = Fieldref           #46.#47        // java/lang/System.out:Ljava/io/PrintStream;
  #10 = Methodref          #48.#49        // java/io/PrintStream.println:(I)V
  #11 = Methodref          #50.#51        // java/util/concurrent/Executors.newFixedThreadPool:(I)Ljava/util/concurrent/ExecutorService;
  #12 = Class              #52            // java/util/concurrent/CountDownLatch
  #13 = Methodref          #12.#53        // java/util/concurrent/CountDownLatch."<init>":(I)V
  #14 = Class              #54            // VolatileTest
  #15 = Class              #55            // java/lang/Object
  #16 = Utf8               InnerClasses
  #17 = Utf8               executorService
  #18 = Utf8               Ljava/util/concurrent/ExecutorService;
  #19 = Utf8               countDownLatch
  #20 = Utf8               Ljava/util/concurrent/CountDownLatch;
  #21 = Utf8               race
  #22 = Utf8               I
  #23 = Utf8               <init>
  #24 = Utf8               ()V
  #25 = Utf8               Code
  #26 = Utf8               LineNumberTable
  #27 = Utf8               increase
  #28 = Utf8               main
  #29 = Utf8               ([Ljava/lang/String;)V
  #30 = Utf8               StackMapTable
  #31 = Utf8               Exceptions
  #32 = Class              #56            // java/lang/InterruptedException
  #33 = Utf8               access$000
  #34 = Utf8               ()Ljava/util/concurrent/CountDownLatch;
  #35 = Utf8               <clinit>
  #36 = Utf8               SourceFile
  #37 = Utf8               VolatileTest.java
  #38 = NameAndType        #19:#20        // countDownLatch:Ljava/util/concurrent/CountDownLatch;
  #39 = NameAndType        #23:#24        // "<init>":()V
  #40 = NameAndType        #21:#22        // race:I
  #41 = NameAndType        #17:#18        // executorService:Ljava/util/concurrent/ExecutorService;
  #42 = Utf8               VolatileTest$1
  #43 = Class              #57            // java/util/concurrent/ExecutorService
  #44 = NameAndType        #58:#59        // execute:(Ljava/lang/Runnable;)V
  #45 = NameAndType        #60:#24        // await:()V
  #46 = Class              #61            // java/lang/System
  #47 = NameAndType        #62:#63        // out:Ljava/io/PrintStream;
  #48 = Class              #64            // java/io/PrintStream
  #49 = NameAndType        #65:#66        // println:(I)V
  #50 = Class              #67            // java/util/concurrent/Executors
  #51 = NameAndType        #68:#69        // newFixedThreadPool:(I)Ljava/util/concurrent/ExecutorService;
  #52 = Utf8               java/util/concurrent/CountDownLatch
  #53 = NameAndType        #23:#66        // "<init>":(I)V
  #54 = Utf8               VolatileTest
  #55 = Utf8               java/lang/Object
  #56 = Utf8               java/lang/InterruptedException
  #57 = Utf8               java/util/concurrent/ExecutorService
  #58 = Utf8               execute
  #59 = Utf8               (Ljava/lang/Runnable;)V
  #60 = Utf8               await
  #61 = Utf8               java/lang/System
  #62 = Utf8               out
  #63 = Utf8               Ljava/io/PrintStream;
  #64 = Utf8               java/io/PrintStream
  #65 = Utf8               println
  #66 = Utf8               (I)V
  #67 = Utf8               java/util/concurrent/Executors
  #68 = Utf8               newFixedThreadPool
  #69 = Utf8               (I)Ljava/util/concurrent/ExecutorService;
{
  public static volatile int race;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC, ACC_VOLATILE

  public VolatileTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #2                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 5: 0

  public static void increase();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #3                  // Field race:I
         3: iconst_1
         4: iadd
         5: putstatic     #3                  // Field race:I
         8: return
      LineNumberTable:
        line 14: 0
        line 15: 8

  public static void main(java.lang.String[]) throws java.lang.InterruptedException;
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=1
         0: iconst_0
         1: istore_1
         2: iload_1
         3: bipush        10
         5: if_icmpge     29
         8: getstatic     #4                  // Field executorService:Ljava/util/concurrent/ExecutorService;
        11: new           #5                  // class VolatileTest$1
        14: dup
        15: invokespecial #6                  // Method VolatileTest$1."<init>":()V
        18: invokeinterface #7,  2            // InterfaceMethod java/util/concurrent/ExecutorService.execute:(Ljava/lang/Runnable;)V
        23: iinc          1, 1
        26: goto          2
        29: getstatic     #1                  // Field countDownLatch:Ljava/util/concurrent/CountDownLatch;
        32: invokevirtual #8                  // Method java/util/concurrent/CountDownLatch.await:()V
        35: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
        38: getstatic     #3                  // Field race:I
        41: invokevirtual #10                 // Method java/io/PrintStream.println:(I)V
        44: return
      LineNumberTable:
        line 18: 0
        line 19: 8
        line 18: 23
        line 29: 29
        line 30: 35
        line 31: 44
      StackMapTable: number_of_entries = 2
        frame_type = 252 /* append */
          offset_delta = 2
          locals = [ int ]
        frame_type = 250 /* chop */
          offset_delta = 26
    Exceptions:
      throws java.lang.InterruptedException

  static java.util.concurrent.CountDownLatch access$000();
    descriptor: ()Ljava/util/concurrent/CountDownLatch;
    flags: ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=1, locals=0, args_size=0
         0: getstatic     #1                  // Field countDownLatch:Ljava/util/concurrent/CountDownLatch;
         3: areturn
      LineNumberTable:
        line 5: 0

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=3, locals=0, args_size=0
         0: bipush        10
         2: invokestatic  #11                 // Method java/util/concurrent/Executors.newFixedThreadPool:(I)Ljava/util/concurrent/ExecutorService;
         5: putstatic     #4                  // Field executorService:Ljava/util/concurrent/ExecutorService;
         8: new           #12                 // class java/util/concurrent/CountDownLatch
        11: dup
        12: bipush        10
        14: invokespecial #13                 // Method java/util/concurrent/CountDownLatch."<init>":(I)V
        17: putstatic     #1                  // Field countDownLatch:Ljava/util/concurrent/CountDownLatch;
        20: iconst_0
        21: putstatic     #3                  // Field race:I
        24: return
      LineNumberTable:
        line 7: 0
        line 9: 8
        line 11: 20
}
SourceFile: "VolatileTest.java"
InnerClasses:
     static #5; //class VolatileTest$1

我们主要看increase()方法中的字节码即可。在字节码中使我们知道虽然在源码中我们只写了一条语句,但是在字节码执行时,是需要5条指令来实现自增运算的。虽然volatile变量可以保证在虚拟机执行字节码时变量是最新的值,但是并不能保证在执行其它指令时,别的线程不在更新volatile变量。如果在此线程没有执行自增运算指令时,其它线程已经对volatile变量进行了修改,那么在此线程在执行自增运算时,得到的volatile变量已经是过期的了,既然是过期的值,那就一定会比真正的值要小,于是此线程就把已经过期的volatile变量的值同步到了主内存中,所以程序在执行时,最后的执行结果会比预计执行的值要小。

  • 使用volatile变量的别一种特性就是禁止指令重排序优化。重排序的意思是说CPU在执行指令时,为了进行优化处理,常常会对指令进行重新排序,也就是说CPU执行指令的顺序在重排序后是有可能和程序源码中的顺序是不一样的,但CPU并不会影响程序原有的运行结果。
0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧