首页 > 代码库 > 理解volatile

理解volatile

<style></style><style></style>

理解volatile

平时工作中对于多线程的应用并不太多,但是不能说工作中不应用就可以对此不去了解,至少要做的知道有这么个东西,主要是作什么的,这样有助于看其它人写的代码。提到这个volatile,一般都会想到并发,同步,锁之类,但要想搞清楚需要看看下面一些知识。

处理器,高速缓存,主内存之间的关系

技术分享

高速缓存的作用是什么?
由于处理器与主内存在处理数据的速度上有数量级的差异,所以引入了比主内存速度更快的高速缓存。处理器从主内存中读取数据放到高速缓存中做交互运算,最后回写到主内存中。
引入高速缓存会带来哪些问题?

  • 使计算机系统更加复杂,但相对带来的优点还是值得的。
  • 缓存一致性问题
    多个处理器如果操作的是同一个主内存中的变量,那么就会出现以谁为准的问题。这就要靠一些规定的协议来维护。

JAVA线程,工作内存,主内存之间的关系

技术分享

这是JAVA内存模型范畴,主要是用来屏蔽硬件与系统的内存访问差异,让JAVA程序可以在不同的平台上达到相同的内存访问效果。

这里说的工作内存,主内存与JVM内存中讲的JAVA堆,栈,方法区不是同一层次上的概念,需要区分。

内存交互操作

主要是工作内存与主内存之间的具体交互协议,即一个变量是如何从主内存加载到工作内存,然后从工作内存如何同步到主内存的具体实现细节,总共有以下几个操作:

技术分享

  • lock,标识一个变量被某个变更独占
  • write,将工作内存中的变量回写到主内存中。这点是volatile的关键,它能够保证被标记了volatile的变量一旦被修改马上执行回写主内存的操作,从而保证其它线程的可见性。

原子性,可见性,有序性

这三个特性是并发操作中需要处理的问题,volatile与下面两个特性有关联。所以在符合可见性以及有序性特性的场景就是volatile的适用场景,也是它的作用所在。

技术分享

  • 原子性
    上面提到的内存交互操作中除了两个锁相关的都可以认为是原子性操作。
  • 可见性
    意思是说对于某个共享的变量,一个线程对其做了修改之后其它线程立马可见。volatile在可见性方面上相对普通的变量有着重大区别,它能够确保共享变量被修改后马上执行回写主内存的操作,而普通的变量做不到。
  • 有序性
    这里有一个有意思的东西就是重排序,它的大体意思就是编写的代码顺序不一定就是最终被处理器执行的顺序,这是为了处理器内部的运算单元能够尽量的被充分利用,有兴趣的可以仔细研究下。而使用了volatile的变量能够确保不被重排序,这是与普通变量不同的第二个重要区别。

volatile的适用场景

  • 运算的结果不依赖共享变量当前的值。

反例,多线程对volatile静态变量执行累加。
这里的count++看起来是一个语句,但对应的字节码不是一条,在执行多条字节码的期间主内存中的值有可能被其它线程所修改,从而导致回写到内存中的值不一致。下面代码的输出并不总是正确。

private static volatile int count=0;private static void increase(){    count++;}public static void main(String[] args) throws Exception {    Thread[] threads=new Thread[30];    for(int i=0;i<threads.length;i++){        threads[i]=new Thread(new Runnable() {            @Override            public void run() {                for(int k=0;k<10000;k++){                    increase();                    System.out.println(count);                }            }        });        threads[i].start();        Thread.sleep(1);    }}

下面是count++对应的字节码,很明显是多条字节码。volatile只能保证每次读取变量的值是最新的,它在获取到主内存变量后是对其副本进行修改,并不会锁定主内存中的值。

 static void access$000();    flags: ACC_STATIC, ACC_SYNTHETIC    Code:      stack=0, locals=0, args_size=0         0: invokestatic  #2                  // Method increase:()V         3: return              LineNumberTable:        line 6: 0  static int access$100();    flags: ACC_STATIC, ACC_SYNTHETIC    Code:      stack=1, locals=0, args_size=0         0: getstatic     #1                  // Field count:I         3: ireturn             LineNumberTable:        line 6: 0  static {};    flags: ACC_STATIC    Code:      stack=1, locals=0, args_size=0         0: iconst_0               1: putstatic     #1                  // Field count:I         4: return              LineNumberTable:        line 8: 0}

某些共享的状态变量是非常适合的,比如dubbo提供的accesslog filter。某些资源只加载一次的场景特别适用,比如应用程序的配置文件的加载,变量的初始化之类。

private volatile ScheduledFuture<?> logFuture = null;private void init() {    if (logFuture == null) {        synchronized (logScheduled) {            if (logFuture == null) {                logFuture = ...;            }        }    }}
  • 也不需要与其它的变量共同参与不变约束

volatile变量与普通的变量在执行性能上的区别

由于volatile需要在修改变量时增加内存屏障语句,理所当然的相对没有内存屏障语句的普通变量要慢一些。

volatile与同步语法块,锁的区别

volatile的特点是当线程对变量修改后马上回与内存保证可见性,同时禁止重排序保证程序执行的有序性。由于它操作的是副本并不会对主内存加锁,所以并不具体同步语法块以及锁的特点即可一时刻同一变量只允许一个线程操作。

引用

本文主要引用《深入理解JAVA虚似机》

理解volatile