Java单例模式和volatile关键字
单例模式是最简单的设计模式,实现也非常“简单”。一直以为我写没有问题,直到被 Coverity 打脸。
1. 暴露问题
前段时间,有段代码被 Coverity 警告了,简化一下代码如下,为了方便后面分析,我在这里标上了一些序号:
private static SettingsDbHelper sInst = null;public static SettingsDbHelper getInstance(Context context) {if (sInst == null) { // 1
synchronized (SettingsDbHelper.class) { // 2
SettingsDbHelper inst = sInst; // 3
if (inst == null) { // 4
inst = new SettingsDbHelper(context); // 5
sInst = inst; // 6
}}}return sInst; // 7
}
大家知道,这可是高大上的 Double Checked locking 模式,保证多线程安全,而且高性能的单例实现,比下面的单例实现,“逼格”不知道高到哪里去了:
private static SettingsDbHelper sInst = null;public static synchronized SettingsDbHelper getInstance(Context context) {if (sInst == null) {
sInst = new SettingsDbHelper(context);}return sInst;}
你一个机器人竟敢警告我代码写的不对,我一度怀疑它不认识这种写法(后面将证明我是多么幼稚,啪。。。)。然后,它认真的给我分析这段代码为什么有问题,如下图所示:

2. 原因分析
Coverity 是静态代码分析工具,它会模拟其实际运行情况。例如这里,假设有两个线程进入到这段代码,其中红色的部分是运行的步骤解析,开头的标号表示其运行顺序。关于 Coverity 的详细文档可以参考这里,这里简单解析一下其运行情况如下:
- 线程 1 运行到 1 处,第一次进入,这里肯定是为
true的; - 线程 1 运行到 2 处,获得锁
SettingsDbHelper.class; - 线程 1 运行到 3 和 4 处,赋值
inst = sInst,这时 sInst 还是 null,所以继续往下运行,创建一个新的实例; - 线程 1 运行到 6 处,修改 sInst 的值。这一步非常关键,这里的解析是,因为这些修改可能因为和其他赋值操作运行被重新排序(Re-order),这就可能导致先修改了 sInst 的值,而
new SettingsDbHelper(context)这个构造函数并没有执行完。而在这个时候,程序切换到线程 2; - 线程 2 运行到 1 处,因为第 4 步的时候,线程 1 已经给 sInst 赋值了,所以
sInst == null的判断为false,线程 2 就直接返回 sInst 了,但是这个时候 sInst 并没有被初始化完成,直接使用它可能会导致程序崩溃。
上面解析得好像很清楚,但是关键在第 4 步,为什么会出现 Re-Order?赋值了,但没有初始化又是怎么回事?这是由于 Java 的内存模型决定的。问题主要出现在这 5 和 6 两行,这里的构造函数可能会被编译成内联的(inline),在 Java 虚拟机中运行的时候编译成执行指令以后,可以用如下的伪代码来表示:
inst = allocat(); // 分配内存
sInst = inst;
constructor(inst); // 真正执行构造函数
说到内存模型,这里就不小心触及了 Java 中比较复杂的内容——多线程编程和 Java 内存模型。在这里,我们可以简单的理解就是,构造函数可能会被分为两块:先分配内存并赋值,再初始化。关于 Java 内存模型(JMM)的详解,可以参考这个系列文章 《深入理解Java内存模型》,一共有 7 篇(一,二,三,四,五,六,七)。
3. 解决方案
上面的问题的解决方法是,在 Java 5 之后,引入扩展关键字 volatile 的功能,它能保证:
对
volatile变量的写操作,不允许和它之前的读写操作打乱顺序;对volatile变量的读操作,不允许和它之后的读写乱序。
关于 volatile 关键字原理详解请参考上面的 深入理解内存模型(四)。
所以,上面的操作,只需要对 sInst 变量添加 volatile 关键字修饰即可。但是,我们知道,对 volatile 变量的读写操作是一个比较重的操作,所以上面的代码还可以优化一下,如下:
private static volatile SettingsDbHelper sInst = null; // <<< 这里添加了 volatile
public static SettingsDbHelper getInstance(Context context) {
SettingsDbHelper inst = sInst; // <<< 在这里创建临时变量
if (sInst == null) {synchronized (SettingsDbHelper.class) {
inst = sInst;if (inst == null) {
inst = new SettingsDbHelper(context);
sInst = inst;}}}return inst; // <<< 注意这里返回的是临时变量
}
通过这样修改以后,在运行过程中,除了第一次以外,其他的调用只要访问 volatile 变量 sInst 一次,这样能提高 25% 的性能(Wikipedia)。
有读者提到,这里为什么需要再定义一个临时变量 inst?通过前面的对 volatile 关键字作用解释可知,访问 volatile 变量,需要保证一些执行顺序,所以的开销比较大。这里定义一个临时变量,在 sInst 不为空的时候(这是绝大部分的情况),只要在开始访问一次 volatile 变量,返回的是临时变量。如果没有此临时变量,则需要访问两次,而降低了效率。
最后,关于单例模式,还有一个更有趣的实现,它能够延迟初始化(lazy initialization),并且多线程安全,还能保证高性能,如下:
class Foo {private static class HelperHolder {public static final Helper helper = new Helper();}public static Helper getHelper() {return HelperHolder.helper;}}
延迟初始化,这里是利用了 Java 的语言特性,内部类只有在使用的时候,才回去加载,从而初始化内部静态变量。关于线程安全,这是 Java 运行环境自动给你保证的,在加载的时候,会自动隐形的同步。在访问对象的时候,不需要同步 Java 虚拟机又会自动给你取消同步,所以效率非常高。
另外,关于 final 关键字的原理,请参考 深入理解Java内存模型(六)。
补充一下,有同学提醒有一种更加 Hack 的实现方式--单个成员的枚举,据称是最佳的单例实现方法,如下:
public enum Foo {
INSTANCE;}
详情可以参考 这里。
4. 总结
在 Java 中,涉及到多线程编程,问题就会复杂很多,有些 Bug 甚至会超出你的想象。通过上面的介绍,开始对自己的代码运行情况都不那么自信了。其实大可不必这样担心,这种仅仅发生在多线程编程中,遇到有临界值访问的时候,直接使用 synchronized 关键字能够解决绝大部分的问题。
对于 Coverity,开始抱着敬畏知心,它是由一流的计算机科学家创建的。Coverity 作为一个程序,本身知道的东西比我们多得多,而且还比我认真,它指出的问题必须认真对待和分析。
使用方法
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@NotThreadSafepublic class NumberRange{ private int lower,upper; public int getLower(){ return lower; } public int getUpper(){ return upper; } public void setLower(int value){ if(value > upper) throw new IllegalArgumentException(...); lower = value; } public void setUpper(int value){ if(value < lower) throw new IllegalArgumentException(...); upper = value; }} |
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
volatile boolean shutdownRequested;...public void shutdown(){ shutdownRequested=true;}public void doWork(){ while(!shutdownRequested) { //dostuff }} |
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public class BackgroundFloobleLoader{ public volatile Flooble theFlooble; public void initInBackground(){ //dolotsofstuff theFlooble = newFlooble(); //this is the only write to theFlooble }}public class SomeOtherClass{ public void doWork(){ while(true){ //dosomestuff... //usetheFlooble,butonlyifitisready if(floobleLoader.theFlooble!=null)doSomething(floobleLoader.theFlooble); } }} |
|
1
2
3
4
5
6
7
8
9
10
11
12
|
public class UserManager{ public volatile String lastUser; public boolean authenticate(String user, String password){ boolean valid = passwordIsValid(user, password); if(valid){ User u = new User(); activeUsers.add(u); lastUser = user; } return valid; }} |
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
@ThreadSafepublic class Person{ private volatile String firstName; private volatile String lastName; private volatile intage; public String getFirstName(){ return firstName; } public String getLastName(){ return lastName; } public int getAge(){ return age; } public void setFirstName(String firstName){ this.firstName = firstName; } public void setLastName(String lastName){ this.lastName = lastName; } public void setAge(int age){ this.age = age; }} |
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
@ThreadSafepublic class CheesyCounter{ //Employs the cheap read-write lock trick //All mutative operations MUST be done with the 'this' lock held @GuardedBy("this") private volatile int value; public int getValue(){ return value; } public synchronized int increment(){ return value++; }} |
Java单例模式和volatile关键字的更多相关文章
- java 轻量级同步volatile关键字简介与可见性有序性与synchronized区别 多线程中篇(十二)
概念 JMM规范解决了线程安全的问题,主要三个方面:原子性.可见性.有序性,借助于synchronized关键字体现,可以有效地保障线程安全(前提是你正确运用) 之前说过,这三个特性并不一定需要全部同 ...
- Java 并发:volatile 关键字解析
摘要: 在 Java 并发编程中,要想使并发程序能够正确地执行,必须要保证三条原则,即:原子性.可见性和有序性.只要有一条原则没有被保证,就有可能会导致程序运行不正确.volatile关键字 被用来保 ...
- Java中的volatile关键字的功能
Java中的volatile关键字的功能 volatile是java中的一个类型修饰符.它是被设计用来修饰被不同线程访问和修改的变量.如果不加入volatile,基本上会导致这样的结果:要么无法编写多 ...
- java中的volatile关键字
java中的volatile关键字 一个变量被声明为volatile类型,表示这个变量可能随时被其他线程改变,所以不能把它cache到线程内存(如寄存器)中. 一般情况下volatile不能代替syn ...
- 深入理解Java中的volatile关键字
在再有人问你Java内存模型是什么,就把这篇文章发给他中我们曾经介绍过,Java语言为了解决并发编程中存在的原子性.可见性和有序性问题,提供了一系列和并发处理相关的关键字,比如synchronized ...
- java并发系列(六)-----Java并发:volatile关键字解析
在 Java 并发编程中,要想使并发程序能够正确地执行,必须要保证三条原则,即:原子性.可见性和有序性.只要有一条原则没有被保证,就有可能会导致程序运行不正确.volatile关键字 被用来保证可见性 ...
- java中的Volatile关键字使用
文章目录 什么时候使用volatile Happens-Before java中的Volatile关键字使用 在本文中,我们会介绍java中的一个关键字volatile. volatile的中文意思是 ...
- 一起来看看java并发中volatile关键字的神奇之处
并发编程中的三个概念: 1.原子性 在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行. 2.可见性 对于可见性,Java提供了volati ...
- Java并发编程 Volatile关键字解析
volatile关键字的两层语义 一旦一个共享变量(类的成员变量.类的静态成员变量)被volatile修饰之后,那么就具备了两层语义: 1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了 ...
随机推荐
- UNDERSTANDING CALLBACK FUNCTIONS IN JAVASCRIPT
转自: http://recurial.com/programming/understanding-callback-functions-in-javascript/ Callback functio ...
- CSS中nth-child和nth-of-type的简单使用
ele:nth-child是查找父元素下的子元素,包括子元素类型非ele的,当子元素类型不是ele时,则不会进行任何操作: ele:nth-of-type是查找父元素下的子元素类型为ele的元素,其是 ...
- 序列dp笔记√
例题: http://www.cnblogs.com/gc812/p/5792181.html http://www.cnblogs.com/gc812/p/5792217.html
- 李洪强iOS开发之【零基础学习iOS开发【01-前言】03-前景和难易度分析
一.iOS开发的前景 2012年3月份,苹果公司的市值已经突破5000亿美元,成为世界上市值最大的公司.5000亿是神马概念呢? 可以帮助陷入欧债危机的8个国家偿还债务 可以买下35个天安门广场.34 ...
- XML DOS 攻击
内容来自此文:http://msdn.microsoft.com/en-us/magazine/ee335713.aspx DoS(Denial of service:拒绝服务)攻击由来已久(1992 ...
- 获取其它进程窗口中的状态栏信息(FindWindowEx GetWindowThreadProcessId OpenProcess SendMessage轮番轰炸)
HWND hWnd = ::FindWindow(NULL, _T("XXXXX")); if(NULL == hWnd) { return ; } HWND hWndStatus ...
- 协同滤波 Collaborative filtering 《推荐系统实践》 第二章
利用用户行为数据 简介: 用户在网站上最简单存在形式就是日志. 原始日志(raw log)------>会话日志(session log)-->展示日志或点击日志 用户行一般分为两种: 1 ...
- 《数据通信与网络》笔记--TCP中的拥塞控制
1.拥塞窗口 发送方窗口的大小不仅取决于接收方,而.而且还取决于网络拥塞的情况. 发送方有2种信息:接收方通告的窗口大小和拥塞窗口的大小,实际的窗口大小事这两者中的最小者. 实际窗口大小 = min( ...
- 构建linux内核源码树
编写驱动程序时,需要内核源码树的支持.内核源码树时从内核源代码编译得到的.下面开始构造内核源代码的步骤.以Ubuntu为例子 1. 下载内源代码,位置www.kernel.org. (注意:源码树内核 ...
- Android开发之EditText属性详解
1.EditText输入的文字为密码形式的设置 (1)通过.xml里设置: 把该EditText设为:android:password="true" // 以”.”形式显示文本 ( ...