单例模式在多线程中的安全性研究
来源:互联网 发布:虚拟机网络设置 编辑:程序博客网 时间:2024/06/15 12:59
概述
关于一般单例模式的创建和分析在我的另一篇博客《Java设计模式——单件模式》中有详细说明。只是在上篇博客中的单例是针对于单线程的操作,而对于多线程却并不适用,本文就从单例模式与多线程安全的角度出发,讲解单例模式在多线程中应该如何被使用。
版权说明
著作权归作者所有。
商业转载请联系作者获得授权,非商业转载请注明出处。
本文作者:Coding-Naga
发表日期: 2016年4月6日
本文链接:http://blog.csdn.net/lemon_tree12138/article/details/51074383
来源:CSDN
更多内容:分类 >> 并发与多线程
目录
- 概述
- 版权说明
- 目录
- 一般情况下的单例模式的创建
- 基于 synchronized 的同步解决方案
- 基于双重检查锁定的解决方案
- 方案分析及测试
- 存在的问题
- 基于 volatile 的解决方案
- 基于类初始化的解决方案
- 基于枚举的解决方案
- Ref
一般情况下的单例模式的创建
首先我们基于单例模式来编写一个Student的类。如下:
Student.java
public class Student { private static Student student = null; private Student() { } public static Student getInstance() { if (student == null) { System.out.println("线程" + Thread.currentThread() + "进入,student = " + student); student = new Student(); } return student; }}
我们将创建学生类的任务交给一个 Runnable 去完成。
CreateRunnable.java
public class Createable implements Runnable { @Override public void run() { Student student = Student.getInstance(); System.out.println("学生类被创建:" + student); System.out.println("Hashcode:" + student.hashCode()); }}
如下是测试代码:
Client.java
public class Client { public static void main(String[] args) { Thread thread1 = new Thread(new Createable()); Thread thread2 = new Thread(new Createable()); thread1.start(); thread2.start(); }}
运行结果
线程Thread[Thread-0,5,main]进入,student = null线程Thread[Thread-1,5,main]进入,student = null学生类被创建:org.naga.demo.thread.singleton.Student@1dbaf954学生类被创建:org.naga.demo.thread.singleton.Student@6df90bbfHashcode:1845038015Hashcode:498792788
从上面程序的运行结果来看,很明显这里创建了两个不同的对象。这与单例模式的定义相悖了。因为在多线程环境下,很明显 getInstance() 方法不能保证原子性,所以这种方法在多线程下是不安全的。
基于 synchronized 的同步解决方案
在一般情况下的单例模式的创建中,我们知道那是一种不安全的创建对象的方案。那么就很容易想到用多线程同步的方法来解决,就是使用关键字 synchronized 来实现同步策略。使用 synchronized 之后的代码及运行结果如下:
Student.java
public synchronized static Student getInstance() { if (student == null) { System.out.println("线程" + Thread.currentThread() + "进入,student = " + student); student = new Student(); } return student; }
运行结果
线程Thread[Thread-0,5,main]进入,student = null学生类被创建:org.naga.demo.thread.singleton.Student@1dbaf954Hashcode:498792788学生类被创建:org.naga.demo.thread.singleton.Student@1dbaf954Hashcode:498792788
从运行结果上可以看出,这里的同步策略是有效的,Thread-0 和 Thread-1 创建的是同一个对象。而关于 synchronized 关键字的详细说明请参见《Java多线程之synchronized和volatile的比较》一文。
不过,对于系统而言,synchronized 同步策略的实现其实是一项性能开销非常大的操作。这可能是 synchronized 需要对对象加锁的缘故。
基于双重检查锁定的解决方案
方案分析及测试
上面说到 synchronized 同步策略对性能开销比较大,对于可能存在大量的 getInstance() 方法调用时,对于系统而言可能就会难以负荷或运行缓慢。这里想到的方法就是减少对 synchronized 关键字的调用。也就是下面要说的双重检查锁定。
Student.java
public class Student { ... ... public static Student getInstance() { System.out.println("线程" + Thread.currentThread() + "进入,student = " + student); if (student == null) { synchronized(Student.class) { if (student == null) { student = new Student(); } } } return student; }}
运行结果-1
线程Thread[Thread-0,5,main]进入,student = null学生类被创建:org.naga.demo.thread.singleton.Student@386f4317学生类被创建:org.naga.demo.thread.singleton.Student@386f4317Hashcode:946815767Hashcode:946815767
运行结果-2
线程Thread[Thread-1,5,main]进入,student = null学生类被创建:org.naga.demo.thread.singleton.Student@1dbaf954Hashcode:498792788线程Thread[Thread-0,5,main]进入,student = org.naga.demo.thread.singleton.Student@1dbaf954学生类被创建:org.naga.demo.thread.singleton.Student@1dbaf954Hashcode:498792788
这里的结果是没有问题的。只是你可能会有疑问,为什么这里采用双重检查锁定?之前我们不是已经对 student 对象进行了判空操作了么,这里怎么还要进行第二次判空?其实在理解了多线程执行的过程,这个问题也就很好回答了。假定有两个线程 T-0 和 T-1,它们现在同时到达第一个 if (student == null) 判空操作,那么这两个线程都可以进入到 if (student == null) 的内部,因为在此之前对象的访问还没有被锁定;这个时候,如果 T-0 获得了锁,并对对象进行初始化操作,结束后释放锁;然后 T-1 获得了 T-0 释放的锁,如果这里不进行第二次判空操作的话,那么 T-1 也会创建一个对象,这个对象与 T-0 创建的是两个完全不同的对象。而如果这里我们进行了第二次判空操作,那么 T-1 得到的对象不为空,就不会再次创建新的对象了。这个方案设计得十分巧妙,既解决了同步带来的性能开销,又保证了单例模式的构建。
存在的问题
对于这一小节,我本人还没有找到一个可以正确测试的方法。这里所作的逻辑说明是来自于《Java 并发编程的艺术》一书。如果你有好的验证方法,欢迎以评论的方式与我交流,共同进步。
这里介绍的双重检查锁定的方案,这的确是一个很巧妙的设计。不过也存在一些细微的问题,这个问题就在于 student = new Student(); 这句代码。对于通过 new 创建对象的过程可以分解成以下3行伪代码。
memory = allocate(); // 1: 分配对象的内存空间ctorInstance(memory); // 2: 初始化对象instance = memory; // 3: 设置 instance 指向刚分配的内存地址
而这里的2、3两个步骤可以被重排序,重排序的结果就像下面的这样:
memory = allocate(); // 1: 分配对象的内存空间instance = memory; // 2: 设置 instance 指向刚分配的内存地址 // 这时,memory处的对象还没有被初始化ctorInstance(memory); // 3: 初始化对象
因为这个重排序的过程,所以这里就有一个问题了。假设有一个线程 T-0 当前执行到上面重排序后伪代码的第2步完成,第3步还没开始时,有一个线程 T-1 进来了,要进行第一次 if (student == null) 判断。因为这里 instance 已经被指向了 memory 分配的地址了。所以,这时 T-1 判断的对象是一个未被初始化的对象。这样就出现了下面这样的输出了。
线程Thread[Thread-1,5,main]进入,student = null学生类被创建:org.naga.demo.thread.singleton.Student@1dbaf954Hashcode:498792788线程Thread[Thread-0,5,main]进入,student = org.naga.demo.thread.singleton.Student@1dbaf954学生类被创建:org.naga.demo.thread.singleton.Student@1dbaf954Hashcode:498792788
尽管如此,我们还是不能直接就说出了问题,因为这里也有可能 T-1 就是在 T-0 对对象创建完成之后才进来的。这里还是看看最佳的实践方案吧。
基于 volatile 的解决方案
上面介绍了双重检查锁定存在的一些弊端,不过我们还是有办法解决的。只要对 student 对象进行 volatile 关键字修饰即可。
Student.java
public class Student { private volatile static Student student = null; ... ...}
运行结果
线程Thread[Thread-0,5,main]进入,student = null学生类被创建:org.naga.demo.thread.singleton.Student@6df90bbf学生类被创建:org.naga.demo.thread.singleton.Student@6df90bbfHashcode:1845038015Hashcode:1845038015
这样就保证了多线程之间,对共享变量的可见性。
基于类初始化的解决方案
在类的初始化阶段(即在Class被加载之后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获得一个锁。这个锁可以同步多个线程对同一个类的同步初始化。
Student.java
public class Student { private Student() { } private static class StudentHolder { private final static Student instance = new Student(); } public static Student getInstance() { return StudentHolder.instance; }}
运行结果
学生类被创建:org.naga.demo.thread.singleton.Student@386f4317学生类被创建:org.naga.demo.thread.singleton.Student@386f4317Hashcode:946815767Hashcode:946815767
基于枚举的解决方案
说到了这里,其实我们还是有一个 bigger 更高的解决方案。那就是使用枚举,使用枚举的好处在于我们不用关心它是否安全,是否真是只有一个实例。下面是采用单例的一些好处:
1. 自由序列化;
2. 保证只有一个实例(即使使用反射机制也无法多次实例化一个枚举量);
3. 线程安全。
Student.java
public enum Student { INSTANCE; private String name; public String getName() { return name; } public void setName(String name) { this.name = name; }}
Createable.java
public class Createable implements Runnable { @Override public void run() { Student student = Student.INSTANCE; System.out.println("学生类被创建:" + student); System.out.println("Hashcode:" + student.hashCode()); }}
运行结果
学生类被创建:INSTANCE学生类被创建:INSTANCEHashcode:1946798030Hashcode:1946798030
使用枚举除了线程安全和防止反射强行调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。因此,Effective Java推荐尽可能地使用枚举来实现单例。
Ref
- 《Java 多线程编程核心技术》
- 《Java 并发编程的艺术》
- 单例模式在多线程中的安全性研究
- 如何保证单例模式在多线程中的线程安全性
- c++单例模式在多线程环境下的安全性
- 单例模式在多线程下的安全性
- 多线程中的单例模式
- 多线程中的单例模式
- Java单例模式在多线程环境中的实现
- Java单例模式在多线程环境中的实现
- Java 单例模式在多线程环境中的实现
- 单例模式在多线程中的使用情况
- 单例模式中的多线程分析
- Servlet 单例多线程,并发安全性
- 单例模式 研究
- 你所不知道的单例模式和多线程并发在单例模式中的影响
- 你所不知道的单例模式和多线程并发在单例模式中的影响
- 多线程中的单件模式
- 多线程安全问题在单例模式中的体现(懒汉式&饿汉式)
- 彻头彻尾理解单例模式及其在多线程环境中的应用
- ColorProgress
- 关于play framework的环境搭建
- iOS开发 cocopods详细使用
- cocos2D-X源码分析之从cocos2D-X学习OpenGL(3)----BATCH_COMMAND
- [iOS]在运行时为类添加方法
- 单例模式在多线程中的安全性研究
- Asp.net下载文件几种方式
- 解决Clock skew detected.
- 史上最全WebView使用,附送Html5Activity一份
- 解决mac osx下pip安装ipython权限的问题
- poj 2229&wustoj 1269划分数(简单dp)
- Spring AOP 详解
- iOS导入三方框架出现"Unknown type name 'NSString'"错误
- ArrayList转换类型为DataTable类型