更多关于设计模式的文章请点击:设计模式之禅(0)-目录页
单件模式是面向对象设计模式中常用的一种设计模式,它主要的作用是使得某个对象在全局程序中只有唯一的一个实例,并且在全局中只有一个创建实例的访问点。这种模式通常采用在线程池、连接池、单一记录器等的使用和实现中。
一、单件模式的实现
单件模式(Singleton Pattern
)在23种设计模式中属于比较简单的那一类,隶属于创建型模式。它的主要思想是在全局中只有一个获得实例的创建点,并且将构造函数私有,不能由外部程序随意地创建它的实例。
1.1 饿汉式
饿汗式的单例模式,顾名思义即在程序启动的时候将单例类直接初始化,这种最简单的一种单例模式实现:
- SingletonObject0
/**
* @Description: 单件模式构造(饿汗式)
* @CreateDate: Created in 2018/11/29 10:54
* @Author: <a href="https://blog.csdn.net/pbrlovejava">arong</a>
*/
public class SingletonObject0 {
private static SingletonObject0 singletonObject = new SingletonObject0();
//构造函数私有,外部类无法直接创建该对象
private SingletonObject0(){
}
//全局唯一的获得实例点
public static SingletonObject0 getInstance() {
return singletonObject;
}
}
饿汗式实现有一个缺点,就是程序启动时即在全局创建了唯一的一个单例对象,但是若程序长时间都没有用过这个对象,会导致其过早初始化,浪费内存的浪费
1.2 懒汉式
懒汉式的实现指的是在程序中有使用到单例对象时才进行初始化。如果以时空角度来看,懒汉式是典型的使用时间换取空间,懒加载会需要时机判断,但是无需一开始初始化,节约内存分配;而饿汉式是典型的空间换取时间,在程序一开始初始化占用了内存,但是却无需进行时机判断。
- SingletonObject1
/**
* @Description: 单件模式构造(懒汉式)
* @CreateDate: Created in 2018/11/29 10:54
* @Author: <a href="https://blog.csdn.net/pbrlovejava">arong</a>
*/
public class SingletonObject1 {
private static SingletonObject1 singletonObject;
//构造函数私有,外部类无法直接创建该对象
private SingletonObject1(){
}
//全局唯一的获得实例点
public static SingletonObject1 getInstance(){
//判断对象是否已经被创建,没有被创建则新建对象
if( singletonObject == null){
singletonObject = new SingletonObject1();
}
return singletonObject;
}
}
通过私有化构造器并且提供唯一的实例获得点,就可以实现SingletonObject在全局中只存在唯一的一个实例了。
@Test
public void fun1() {
SingletonObject1 singletonObject1 = SingletonObject1.getInstance();
SingletonObject1 singletonObject2 = SingletonObject1.getInstance();
System.out.println(singletonObject1+"\n"+singletonObject2);
}
二、在并发下单件模式的改进
2.1 使用闭锁测试单件模式的正确性
上面写的单件模式代码似乎已经可以在全局中只产生一个实例了,可是如果在多线程模式下,以上代码还能实现单件吗?我使用了闭锁来测试并发时是否仍然能获得唯一的单件类:【关于闭锁的使用可以阅读我的一篇文章:Java并发编程(9)-使用闭锁测试并发线程安全性】
@Test
public void concurrentTest(){
//开始闭锁
CountDownLatch startLatch = new CountDownLatch(1);
for (int i = 0; i < 5; i++) {
new Thread() {
public void run(){
try {
//线程运行至闭锁处等待
startLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
//5个线程并发执行任务
new MyTask().run();
}
}.start();
}
//线程全部集结在闭锁下,开锁
startLatch.countDown();
}
//任务类
public class MyTask implements Runnable{
@Override
public void run() {
//获得单例类
SingletonObject1 instance = SingletonObject1.getInstance();
//打印地址
System.out.println(instance);
}
}
在这个测试中,我只让5个线程去并发地获得单件类实例,可是却出现了问题,单件的实例并不唯一了:
2.2 同步式
要使得单件模式在并发条件下仍然正确,最简单的方式就是使用同步锁来限制同时读取实例的线程数,这个做法很安全,但是会影响速度:
- SingletonObject3
/**
* @Description: 单件模式构造(同步式)
* @CreateDate: Created in 2018/11/29 10:54
* @Author: <a href="https://blog.csdn.net/pbrlovejava">arong</a>
*/
public class SingletonObject3 {
private static SingletonObject3 singletonObject;
//构造函数私有,外部类无法直接创建该对象
private SingletonObject3(){
}
//全局唯一的获得实例点
public static synchronized SingletonObject3 getInstance(){
//判断对象是否已经被创建,没有被创建则新建对象
if( singletonObject == null){
singletonObject = new SingletonObject1();
}
return singletonObject;
}
2.3 二重检查加锁
对于同步锁而言,还可以使用更加轻量的volatile
关键字来实现线程之间对实例状态的检查加锁:
public class SingletonObjec4 {
private volatile static SingletonObject4 singletonObject;
//构造方法私有化,只能在本类中调用构造方法
private SingletonObject(){
}
/**
*@description 获得全局唯一的实例(二重检查加锁)
*@author arong
*@date 2018/11/27
*@param:
*@return com.iteason.singletonPattern.SingletonObject
*/
public static SingletonObject4 getInstance(){
//一重检查
if(singletonObject == null) {
//加锁
synchronized (singletonObject){
//二重检查
if (singletonObject == null) {
singletonObject = new SingletonObject();
}
}
}
return singletonObject;
}
}
2.4. 典型错误-缺失volatile
在双重检查加锁的单例模式中,如果单例对象不使用volatile进行修饰的话,因为创建对象的重排序的原因,将初始化和分配内存这两个步骤颠倒,那么会导致创建对象时的误判,导致返回一个空对象。
一个错误的双重检查加锁Demo
public class Singleton {
private Singleton instance = null;
private Singleton() {}
public Singleton getInstance() {
// 先判断对象是否已经初始化
if (instance == null) {
synchronized (instance) {
// 确定对象没有初始化
if (instance == null) {
return new Singleton();
}
}
}
return object;
}
}
上述代码发生错误的原因在于return new Singleton()
这行代码,这行代码并不是一个原子性的的操作,它可以分为以下三个阶段:
1.分配对象内存
2.初始化对象
3.将对象引用到内存空间
其中,在将对象引用到内存空间时,instance就不等于null了。
在单线程情况下,2和3指令发生重排序,但是由于as-if-serial语义,其执行结果也是不变的:
在多线程情况下,有可能会发生以下的执行顺序,导致程序执行结果出错:
A线程在执行到第3步的时候即设置instance指向内存空间,此时B线程判断其不为空,返回instance,但此时instance并未进行初始化,所以出现了错误,返回了一个空对象。
解决该问题的方法是在instance前加上volatile修饰,这样就能禁止重排序。