多线程并发下的单例模式
2017-06-26 14:55
190 查看
定义:
单例模式是设计模式中最简单的形式之一。这一模式的目的是使得类的一个对象成为系统中的唯一实例。下面通过代码分析下java中,各种单例模式写法的优缺点。
1、饿汉模式
示例1.1
public class Singleton { private Singleton() {} private static Object INSTANCE = new Object(); public static Object getInstance() { return INSTANCE; } }
在类生命周期的【初始化】阶段进行生成单例对象(类的初始化阶段会对静态变量赋值),当执行类初始化的阶段是需要先获得锁才能进行初始化操作,而且一个class类只进行初始化一次。类初始化阶段是线程安全的,JVM保证类初始化只执行一次。这样可以确保只生成一个对象。
类声明周期分为:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸御(Unloading)。
类的生命周期不明白的请查看:JVM 类加载机制深入浅出
类加载后不一定马上执行初始化阶段。当遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。
new 创建对象操作
getstatic 访问类的静态变量操作
putstatic 给类的静态变量赋值操作
invokestatic 调用静态方法操作
这个饿汉模式中,不会出现new、invokestatic和putstatic指令,外面的类只能调用 getInstance()静态方法,由此推断,此单例模式也是延迟加载对象的,只有第一次调用getInstance()静态方法,才会触发他的初始化阶段,才会创建单例对象。
其实这个例子应该是懒汉模式,只有在第一次使用的时候才加载
下面这个【示例1.2】不是延迟加载单例对象
示例1.2
public class Singleton { private Singleton() {} private static int count=0; private static Object INSTANCE = new Object(); public static Object getInstance() { return INSTANCE; } }
当程序先调用Singleton1中的count属性时(getstatic 或putstatic 指令),就会执行类的【初始化】阶段,会生成单例对象,而不是调用getInstance()静态方法才生成单例对象。
示例1.3 (静态内部类实现方式)
public class Singleton { private Singleton() {} private static int count=0; private static class SingletonHolder{ private static final Object INSTANCE = new Object(); } public static Object getInstance(){ return SingletonHolder.INSTANCE; } }
使用内部类SingletonHolder来防止【示例1.2】出现的问题,防止其它的变量的干扰,导致提前触发类声明周期中的【初始化】阶段来创建INSTANCE 实例。
Effective Java中推荐的单例写法
2、懒汉模式
示例2.1
public class Singleton{ private Singleton() { } private static Object INSTANCE = null; public static Object getInstance() { if(INSTANCE == null){ INSTANCE = new Object(); } return INSTANCE; } }
每次创建INSTANCE 的时候先判断是否null,如果为null则new一个,否则就直接返回INSTANCE 。当多线程工作的时候,如果有多个线程同时运行到if (INSTANCE == null),都判断为null,那么两个线程就各自会创建一个实例。这样就会创建多一个实例,这样就不是单例了。
下面的【示例2.2】加上synchronized 改进多线程并发引起的问题
示例2.2 (synchronized 实现方式)
public class Singleton { private Singleton() { } private static Object INSTANCE = null; public synchronized static Object getInstance() { if(INSTANCE == null){ INSTANCE = new Object(); } return INSTANCE; } }
虽然synchronized 能解决多线程同时并发引起的问题,但是每次访问该方法都需要获得锁,性能大大降低。其实只要创建INSTANCE 实例后就不需要加锁的,直接获取该对象就ok。
示例2.3 (双重检查实现方式)
public class Singleton { private Singleton() { } private static Object INSTANCE = null; public static Object getInstance() { if(INSTANCE == null){ synchronized(Singleton3.class){ if(INSTANCE == null){ INSTANCE = new Object(); } } } return INSTANCE; } }
这个版本的代码看起来有点复杂,注意其中有两次if (instance == null)的判断,这个叫做『双重检查 Double-Check』。
第一个if (instance == null),其实是为了解决【示例2.2】中的效率问题,只有instance为null的时候,才进入synchronized的代码段——这样在对象创建后就不会在进入同步代码块了。
第二个if (instance == null),则是跟【示例2.2】一样,是为了防止可能出现多个实例的情况。
从代码层面看似完美,效率问题也解决了。但实际还是有问题,在并发环境下可能会出现instance为null的情况。下面我们来分析下为什么会出现此问题。
原子操作
INSTANCE = new Object();不是原子操作。在JVM中会拆分成3个步骤
1、分配对象的内存空间
2、初始化对象
3、设置INSTANCE 指向刚分配的内存地址
指令重排
指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。可以参考:java内存模型
【2、初始化对象和 3、设置INSTANCE 指向刚分配的内存地址】这两个操作可能发生重排序。
如下图:
从图中可以看出A2和A3的重排序,将导致线程
B在B1处判断出instance不为空,线程B接下来将访问instance引用的对象。此时,线程B将会访
问到一个还未初始化的对象。
示例2.4 (基于volatile的解决方案)
public class Singleton { private Singleton() {} private static volatile Object INSTANCE = null; public static Object getInstance() { if(INSTANCE == null){ synchronized(Singleton.class){ if(INSTANCE == null){ INSTANCE = new Object(); } } } return INSTANCE; } }
声明对象的引用为volatile后,【2、初始化对象和 3、设置INSTANCE 指向刚分配的内存地址】之间的重排序,在多线程环境中将会被禁止。
从图表中可以看出volatile可以确保,volatile变量读写顺序,可以保证一个线程写volatile变量完成后(创建完对象后),其它线程才能读取该volatile变量,相当于给这个创建实例的构造上了一把锁。这样,在它的赋值完成之前,就不用会调用读操作。
示例2.5 (枚举实现方式)
public enum Singleton6 { INSTANCE; public String getInfo(String s){ s = "hello " + s; System.out.println(s); return s; } public static void main(String[] args) { String s = INSTANCE.getInfo("aa"); System.out.println(s); } }
这种写法在功能上与共有域方法相近,但是它更简洁,无偿地提供了序列化机制,绝对防止对此实例化,即使是在面对复杂的序列化或者反射攻击的时候。虽然这中方法还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。
本人简书blog地址:http://www.jianshu.com/u/1f0067e24ff8
点击这里快速进入简书
相关文章推荐
- 高性能高并发服务器架构浅析--多线程模式
- 单例模式在多线程并发情况下的一些思考。
- 多线程(八)---单例模式并发访问
- 你所不知道的单例模式和多线程并发在单例模式中的影响
- 生产者--消费者模式,理解多线程并发
- ASP.NET 环境下的并发与多线程处理及单件模式、事件重入
- 饿汉式单例模式在多线程中并发访问的解决方案
- Java多线程之并发协作生产者消费者设计模式
- Java多线程之并发协作生产者消费者设计模式
- Java 并发编程 多线程的交互模式
- future相比于其他并发设计模式的优势(“回调驱动(多线程环境下)”、“消息/事件驱动(Actor模型中))
- 你所不知道的单例模式和多线程并发在单例模式中的影响
- 基于多线程并发的单例模式
- JAVA多线程并发下的单例模式应用
- Java多线程Thread-并发协作(生产者消费者设计模式)
- JAVA并发设计模式学习笔记(一)—— JAVA多线程编程
- JAVA并发处理经验(四)并行模式与算法6:socket的服务端多线程
- Java多线程之并发协作生产者消费者设计模式
- Java多线程之并发协作生产者消费者设计模式
- 你所不知道的单例模式和多线程并发在单例模式中的影响