`

Java中的Volatile关键字详解

 
阅读更多

前两天,并行实验室发表了一篇有关volatile关键字的文章[1],而该文参考文献中的Sayonara volatile[2]则更直接地要与volatile再见。两篇文章不谋而合,从c/c++到Java/.net,将这几门语言中的volatile过了一遍。列举出各种乱象,看上去volatile是如此不堪。

确实,volatile的语义不是那么常见。不是锁,但是它又拥有锁的部分特性(可见性,Java 5以后还有顺序保证)。如果不能正确理解,确实是个容易出错的地方。但是如果理解了用对了,仅就Java 5以及以后的版本而言,在目前阶段,volatile对性能上的提升还是有帮助的。

Volatile在Java中的语义以及历史

维基百科上,对Java中的Volatile给了一个准确的定义[6]

  • (In all versions of Java) There is a global ordering on the reads and writes to a volatile variable. This implies that every thread accessing a volatile field will read its current value before continuing, instead of (potentially) using a cached value. (However, there is no guarantee about the relative ordering of volatile reads and writes with regular reads and writes, meaning that it’s generally not a useful threading construct.)
  • (In Java 5 or later) Volatile reads and writes establish a happens-before relationship, much like acquiring and releasing a mutex.

翻译过来就是:

  • (在所有的Java版本中)对volatile变量上的读和写有一个全局排序。这暗示着每个线程访问一个volatile域时在往下运行之前将读到当前的值,而不是(有可能)使用一个缓存值。(然而,在volatie变量的读写与常规的读写之间的排序没有保证,意味着volatile通常不是一个有用的线程化的构造)
  • (在Java5及之后版本中) Volatile读和写建立了一种happens-before关系,就像获取和释放一个互斥体。

在Java中,要正确地并发,必须在原子性、可见性和顺序性三个方面做出保证。在Java 5以前的版本中,volatile就已经实现了可见性,但是因为不能保证与普通变量读写之间的顺序,故而经常是没有用的。在Java 5以后的版本中,则额外保证了其与普通变量读写的顺序性。这里不是特别好懂,我们将javamx上的例子[3] [4]改造一下:

public class MyBrokenFactory {
  private static volatile MyFactory instance;
  private int field1, field2 ...
 
  public static MyBrokenFactory getFactory() {
    // This is incorrect: don't do it at home, kids!
    if (instance == null) {
      synchronized (MyBrokenFactory.class) {
        if (instance == null)
          instance = new MyBrokenFactory();
      }
    }
    return instance;
  }
 
  private MyBrokenFactory() {
    field1 = ...
    field2 = ...
  }
}

上面的代码,在Java5以前,这个例子是不能正确工作的,因为volatile不保证对field1,field2的初始化(常规读写)一定在对volatile变量instance的写之前完成。但双检查锁本身并非volatile导致的,反而是Java 1.5的volatile让双检查锁能正确工作了。并行实验室将双检查锁不正确算在volatile的头上,有点冤了。

正是因为volatile在Java 5以前没有得到正确实现,更添加了不少复杂性。在Java 5之前,volatile基本没用处。

Volatile的实现机制分析

在Hotspot JVM中,
a) 在JVM层次,对volatile变量在线程本地工作区中不做缓存,对volatile的读写总是指向堆中的引用。可以视作在一个assign指令后总是跟着一个store指令[5]
b) 在机器码执行层次,通过内存屏障指令等迫使CPU不重排序,清除缓存,详细请参考《Memory Barriers and JVM Concurrency》的分析。

这与synchronized是有明显不同的,synchronized通常需要原子锁定,在SMP上要通过锁定总线等方式来实现,其代价在大多数平台上通常要比volatile高得多。

Performance

图一运行100万次
引入Volatile的主要目的,就是为了性能。我分下面几个方面来谈谈volatile在目前的Java平台中的作用。

直接性能比较

简单写了一个测试来比较一下volatile以及它的两种替代品 — 原子类型(atomic)和锁(此处指synchronized)。例子的代码改编自庄周梦蝶slides《Java NIO trick and trap》中的SystemTimer(部分代码省略,完整源码在Github上)。这个测试只是一个例子,但其实在网络库里都要做定时,原理也大抵如此。

第一个版本如下:

package me.xiping.volitileverify;
public class SystemTimerV1 {
	private final static ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
	private static final long tickUnit = Long.parseLong(System.getProperty(
			"notify.systimer.tick", "50"));
	private static volatile long time = System.currentTimeMillis();
 
	private static class TimerTicker implements Runnable {
		public void run() {
			time = System.currentTimeMillis();
		}
	}
 
	public static long currentTimeMillis() {
		return time;
	}
 
	static {
		executor.scheduleAtFixedRate(new TimerTicker(), tickUnit, tickUnit,
				TimeUnit.MILLISECONDS);
		Runtime.getRuntime().addShutdownHook(new Thread() {
			@Override
			public void run() {
				executor.shutdown();
			}
		});
	}
}

代码的重点在于volatile修饰的time域。time是一个只有一个线程写,其他线程都是读的域,没有并发修改,这里要解决的就是可见性。这正是volatile最适合的场合。

第二个版本使用synchronize关键字,与版本V1的不同如下:

package me.xiping.volitileverify;
public class SystemTimerV2 {
	....
	private static long time = System.currentTimeMillis();
 
	private static class TimerTicker implements Runnable {
		public void run() {
			synchronized (SystemTimerV2.class) {
				time = System.currentTimeMillis();
			}
		}
	}
 
	public static synchronized long currentTimeMillis() {
		return time;
	}
	....
}

第二个版本使用Java的内部锁来同步,既保证了原子性,也保证了可见性。第三个版本则是使用AtomicLong来实现的,如下:

package me.xiping.volitileverify;
 
public class SystemTimerV3 {
	......
	private static AtomicLong time = new AtomicLong(System.currentTimeMillis());
 
	private static class TimerTicker implements Runnable {
		public void run() {
			time.set(System.currentTimeMillis());
		}
	}
 
	public static long currentTimeMillis() {
		return time.get();
	}
	.....
}

在我的本上(cpu: 2core; OS: win7/cygwin; java:1.6.0_13)运行100万次,其性能如上面右图一所示(单位:ms)。
图二运行1千万次
运行1000万次性能如图二所示(注意纵坐标的值与图一不一样)。

结论:性能上的优越性是显而易见的。volatile比AtomicLong明细要快,相比于synchronized则有几倍几十倍的提高了。
(欢迎在不同平台下测试并在评论中给一个反馈,反馈时请包括处理器信息(平台,core数),OS和Java版本。源代码在这里。编译和运行需要Ant,具体指南请参考这里。)

[Update, Dec15th: 图片是用Google Chart API 生成的,在源代码中包含着两个自动执行测试并生成图片URL的脚本。脚本需运行在Bash环境下,Windows上需在Cygwin下运行。]

Mina/netty对volatile的利用

写博文时顺便统计了下volatile在一些性能比较重要的程序中的情况,我顺手统计了手边有的mina和netty两个项目的核心代码,数据如下:

Project sources path synchronized occurrence volatile occurrence
netty src/main/java 150 177
mina src/main/java 216 55

其中,mina是2009-3-31日Subversion的trunk,如下:

$svn info
Path: .
URL: http://svn.apache.org/repos/asf/mina/trunk
Repository Root: http://svn.apache.org/repos/asf
Repository UUID: 13f79535-47bb-0310-9956-ffa450edef68
Revision: 761900
Node Kind: directory
Schedule: normal
Last Changed Author: jvermillard
Last Changed Rev: 760493
Last Changed Date: 2009-03-31 23:49:15 +0800 (Tue, 31 Mar 2009)

netty的版本信息是:

$git log
commit 1ffb1aea75c36def56b709bd0892b19df78d9249
Author: Trustin Lee <trustin@gmail.com>
Date:   Fri Nov 12 10:20:03 2010 +0900

从侧面证明了volatile其实是很重要的,在性能很重要的场合,应用还是比较多的。

结论

总而言之,相比与Atomic和synchronized,volatile在性能上还是有显著的优势。

不过,正如文章中开头讨论的那样,volatile的缺点也显而易见,需要开发者对volatile的应用需要对内存模型的理解,否则容易误解而造成错误。在并发程序中,其仅可用于JVM支持的几个基本类型(int,short,long,double,reference, etc.),而这几个基本类型皆有对应的完整并发特性的包装原子类型(java.util.concurrent.atomic.*),虽然其性能与volatile有些差距,但大部分情形下也可代替volatile(但用synchronized来代替volatile是很不划算的)。

分享到:
评论
2 楼 zdjray 2015-03-19  
东仔在北京 写道
if (instance == null) {
      synchronized (MyBrokenFactory.class) {
        if (instance == null)
          instance = new MyBrokenFactory();
      }
    }


如果instance不是volatile类型的,会出现创建多个的情况么?


不会,但是可能会返回一个错误的实例状态,这时还没有进synchronized就返回了
1 楼 东仔在北京 2012-05-11  
if (instance == null) {
      synchronized (MyBrokenFactory.class) {
        if (instance == null)
          instance = new MyBrokenFactory();
      }
    }


如果instance不是volatile类型的,会出现创建多个的情况么?

相关推荐

    java里的volatile关键字详解

    java里的volatile关键字详解

    Java中Volatile关键字详解及代码示例

    主要介绍了Java中Volatile关键字详解及代码示例,分为两个部分,第一部分介绍了Volatile关键字的基本概念等内容,第二部分分享了实例代码,具有一定参考价值,需要的朋友可以了解下。

    java里的volatile关键字详解.pdf

    java里的volatile关键字详解.pdf

    Java——volatile关键字详解

    volatile是比synchronized关键字更轻量级的同步机制,访问volatile变量时不会执行加锁操作,因此不会使执行线程阻塞。 volatile保证可见性和禁止指令重排序,底层是通过“内存屏障”来实现,但不保证原子性。 写入...

    Java中volatile关键字的作用与用法详解

    volatile关键字虽然从字面上理解起来比较简单,但是要用好不是一件容易的事情。这篇文章主要介绍了Java中volatile关键字的作用与用法详解的相关资料,需要的朋友可以参考下

    Java多线程 volatile关键字详解

    主要介绍了Java多线程 volatile关键字详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下

    Java并发教程之volatile关键字详解

    主要给大家介绍了关于Java并发教程之volatile关键字的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用Java具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧

    详解Java线程编程中的volatile关键字的作用

    主要介绍了Java线程编程中的volatile关键字的作用,针对其禁止进行指令重排序和读写内存方面着重讲解,需要的朋友可以参考下

    Java Volatile关键字同步机制详解

    主要介绍了Java Volatile关键字同步机制详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下

    详解Java面试官最爱问的volatile关键字

    主要介绍了详解Java面试官最爱问的volatile关键字,小编觉得还是挺不错的,具有一定借鉴价值,需要的朋友可以参考下

    java volatile关键字作用及使用场景详解

    在本文里我们给大家分享的是关于java volatile关键字作用及使用场景的相关知识点内容,需要的朋友们学习下。

    java volatile关键字的含义详细介绍

    主要介绍了java volatile关键字的含义详解的相关资料,需要的朋友可以参考下

    Java中的关键字volatile详解

    主要介绍了Java中的关键字volatile,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

    Java线程详解(ppt)

    Java线程详解,这是一个比较好的学习和教学线程的资料

    Java高并发编程详解:多线程与架构设计 (Java核心技术系列)

    第三部分详细、深入地介绍volatile关键字的语义,volatile关键字在Java中非常重要,可以说它奠定了Java核心并发包的高效运行,在这一部分中,我们通过实例展示了如何使用volatile关键字以及非常详细地介绍了Java内存...

    Java 多线程与并发(5-26)-关键字- volatile详解.pdf

    Java 多线程与并发(5_26)-关键字_ volatile详解

    Java面试官最喜欢问的关键字之volatile详解

    主要给大家介绍了关于Java面试官最喜欢问的关键字之volatile的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用Java具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧

    各种java面试大全,包含nginx、mysql、redis、spring各种基础

    hashmap实现原理,java基础笔记,java基础面试全集,java里的volatile关键字详解,jvm垃圾回收,MySQL索引背后的数据结构及算法原理,MySQL性能优化的最佳21条经验,MySQL中的锁(表锁、行锁),Redis的优点和5种...

    java并发编程理论基础精讲

    本资源为您提供了关于 Java 并发编程理论基础的精讲,涵盖了多线程编程的核心概念、基本原理以及在 Java 中的应用。通过深入学习,您将建立坚实的并发编程基础,能够更好地理解和应对多线程编程中的挑战。 并发编程...

    java线程详解

    Java线程:volatile关键字 Java线程:新特征-线程池 一、固定大小的线程池 二、单任务线程池 三、可变尺寸的线程池 四、延迟连接池 五、单任务延迟连接池 六、自定义线程池 Java线程:新特征-有返回值的线程...

Global site tag (gtag.js) - Google Analytics