一、什么是单例模式
单例模式是在内存中只创建一个对象的模式,它保证一个类只有一个实例。
二、单例模式的几种实现
(一)懒汉式单例模式
/**
* 懒汉式单例模式
* (懒加载,需要的时候在去加载)
* 优点:需要SingleObject时,才会去实例化,节省空间
* 缺点:在多线程环境下,getSingleObject() 方法并不是线程安全的
*/
public class SingleObject {
//私有化对象(未实例化)
private static SingleObject object;
//私有化构造方法
private SingleObject() {};
//获取私有化对象
public static SingleObject getSingleObject() {
//bean为null才去实例化,否则直接返回。
if (object == null) {
object = new SingleObject();
}
return object;
}
}
(二)饿汉式单例模式
/**
* 饿汉式单例模式
* (线程安全,但会造成资源浪费)
* 优点:在类加载时候创建实例,不存在线程安全问题
* 缺点:占用系统资源,如果这个实例没有被用到,则会造成资源浪费
*/
public class Single {
//私有化对象(已实例化)
private static Single object = new Single();
//私有化构造方法
private Single() {};
//获取私有化对象
public static Single getSingle() {
return object;
}
}
(三)双重校验加锁式单例模式(线程安全)
public class SingleObject{
//volatile关键字防止指令重排序造成的空指针异常(通过插入特定的内存屏障的方式来禁止指令重排序)
private static volatile SingleObject object;
//私有构造方法
private SingleObject() {}
public static SingleObject getSingleObject() {
//第一次检查防止每次获取bean都加锁
if (object == null) {
//加锁,防止第一次创建实例化时,并发线程多次创建对象
synchronized (SingleObject.class) {
//第二次检查判断对象没有实例化,则进行对象的实例化
if (object == null) {
object = new SingleObject();
}
}
}
return object;
}
}
三、问题分析
(一)懒汉模式为什么会有线程安全问题?
如上图所示,多线程情况下,在时刻T,线程A和线程B都判断single为null,从而进入if代码块中都执行了new Single()的操作创建了两个对象,就和我们当初的单例初衷相悖而行。
(二)为什么要进行两次判空,他们的作用分别是什么?加锁的作用是什么?
1、第一次判空目的:为了缩小锁的粒度,避免每次获取实例都需要进行加锁。
2、加锁目的:在首次获取对象实例时,防止并发线程多次创建对象。
3、第二次判空目的:如果对象没有进行实例化,则进行对象实例化操作
(三)volatile关键字修饰对象的作用是什么?
volatile关键字防止指令重排序造成的空指针异常(通过插入特定的内存屏障的方式来禁止指令重排序)
指令重排序是指:JVM在保证最终结果正确的情况下,可以不按照程序编码的顺序执行语句,尽可能提高程序的性能。
创建一个对象,在JVM中会经过三步:
- (1)为object对象分配内存空间
- (2)初始化object对象
- (3)将object指向分配好的内存空间
由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getSingleObject() 后发现 object不为空,因此返回 object,但此时 object还未被初始化,就会出现空指针异常。