模式介绍
单例对象的类只能允许一个实例存在。
- 单例类只有一个实例对象;
- 该单例对象必须由单例类自行创建;
- 单例类对外提供一个访问该单例的全局访问点。
实现
立即加载 / “饿汉模式”
public class HungrySingleton {
private static final HungrySingleton instance = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return instance;
}
}
饿汉模式的特点是初始化类的时候就创建好了单例,保证在调用 getInstance 方法之前单例已经存在了。由于这个类在整个生命周期中只会被加载一次,因此只会创建一个实例,即能够充分保证单例。
优点: 比较简单,类加载的时候就完成实例化。避免了线程同步问题。
缺点: 在类加载的时候就完成实例化,没有达到Lazy Loading的效果。如果自始至终都未使用过这个实例,则会造成资源的浪费。
延迟加载 / “懒汉模式”
线程不安全
public class LazySingleton {
// 将自身实例化对象设置为一个属性,并用static修饰
private static LazySingleton instance;
// 构造方法私有化
private LazySingleton() {
}
// 静态方法返回该实例
public static LazySingleton getInstance() {
if(instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
线程不安全原因:如上图,多线程情况下,线程A和线程B同时访问 getInstance() 方法。因为 instance 是空,所以两个线程同时通过了条件判断,开始执行 new 操作,这样 instance 就被构建了两次。
优点: 实现起来比较简单,当类 LazySingleton 被加载的时候,静态变量 static 的 instance 未被创建并分配内存空间,当 getInstance 方法第一次被调用时,初始化 instance 变量,并分配内存,因此在某些特定条件下会节约了内存。
缺点: 在多线程环境中,这种实现方法是完全错误的,根本不能保证单例的状态。
线程安全
public class LazySingleton {
// 将自身实例化对象设置为一个属性,并用static修饰
private static LazySingleton instance;
// 构造方法私有化, private 避免类在外部被实例化
private LazySingleton() {
}
// 静态方法返回该实例,加synchronized关键字实现同步
public static synchronized LazySingleton getInstance() {
if(instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
优点:在多线程情形下,保证了“懒汉模式”的线程安全。
缺点:众所周知在多线程情形下,synchronized 方法通常效率低,显然这不是最佳的实现方案。
DCL双检查锁机制
public class DCLSingleton {
private volatile static DCLSingleton instance = null; // 单例对象
// 私有构造函数
private DCLSingleton() {
}
// 静态工厂方法
public static DCLSingleton getInstance() {
if (instance == null) { // 双重检测机制
synchronized (DCLSingleton.class){ // 同步锁
if (instance == null) { // 双重检测机制
instance = new DCLSingleton();
}
}
}
return instance;
}
}
1.为了防止 new DCLSingleton() 被执行多次,因此在 new 操作之前加上 Synchronized 同步锁,锁住整个类(注意,这里不能使用对象锁)。
2.进入 Synchronized 临界区以后,还要再做一次判空。因为当两个线程同时访问的时候,线程A构建完对象,线程B也已经通过了最初的判空验证,不做第二次判空的话,线程B还是会再次构建 instance 对象。
- 第一步 线程A和线程B同时执行 getInstance() 方法
- 第二步 线程B获取不到锁,线程A初始化对象
- 第三步 线程A结束
- 第四步 线程B进行判空校验
- 第五步 线程B结束
volatile
假如没有用 volatile
修饰,会出现啥漏洞?
这里涉及到了JVM编译器的指令重排。
比如java中简单的一句 instance = new DCLSingleton(),会被编译器编译成如下JVM指令:
memory =allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance =memory; //3:设置instance指向刚分配的内存地址
但是这些指令顺序并非一成不变,有可能会经过JVM和CPU的优化,指令重排成下面的顺序:
memory =allocate(); //1:分配对象的内存空间
instance =memory; //3:设置instance指向刚分配的内存地址
ctorInstance(memory); //2:初始化对象
当线程A执行完1,3时,instance 对象还未初始化,但已经不再指向null。此时如果线程B抢占到CPU资源,执行 if(instance == null)的结果会是false,从而返回一个没有初始化完成的instance对象。
静态内部类
public class StaticInnerSingleton {
private static class StaticInner {
private static final StaticInnerSingleton INSTANCE = new StaticInnerSingleton();
}
private StaticInnerSingleton (){
}
public static StaticInnerSingleton getInstance() {
return StaticInner.INSTANCE;
}
}
-
外部无法访问静态内部类 StaticInner,通过调用 StaticInnerSingleton.getInstance() 获取单例对象INSTANCE。
-
此方式并非是在类加载的时候就将 INSTANCE 对象初始化,而是在调用 getInstance() 方法的时候。因此这种实现方式是利用ClassLoader的加载机制来实现懒加载,并保证线程安全。
静态内部类的实现方式依然无法解决利用反射重复创建实例的问题。如下:
public class CrackStaticInner {
public static void main(String[] args) {
//获得构造器
Constructor con = null;
//构造两个不同的对象
StaticInnerSingleton singleton1 = null;
StaticInnerSingleton singleton2 = null;
try {
con = StaticInnerSingleton.class.getDeclaredConstructor();
//设置为可访问
con.setAccessible(true);
singleton1 = (StaticInnerSingleton)con.newInstance();
singleton2 = (StaticInnerSingleton)con.newInstance();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
//验证是否是不同对象
System.out.println("singleton1与singleton2是否是相同实例: " + singleton1.equals(singleton2));
}
}
我们发现,最终的结果是false,两者实例并不是同一个。那如何才能解决反射打破单例的情况出现呢?
枚举
public enum SingletonEnum {
INSTANCE;
}
public class EnumSingleton {
public static void main(String[] args) {
//获得构造器
Constructor con = null;
//构造两个不同的对象
SingletonEnum singleton1 = null;
SingletonEnum singleton2 = null;
try {
con = SingletonEnum.class.getDeclaredConstructor();
//设置为可访问
con.setAccessible(true);
singleton1 = (SingletonEnum)con.newInstance();
singleton2 = (SingletonEnum)con.newInstance();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
//验证是否是不同对象
System.out.println("singleton1与singleton2是否是相同实例: " + singleton1.equals(singleton2));
}
}
执行,在获取构造器的时候,程序抛出异常:
使用枚举实现单例:
public enum SingletonEnum {
INSTANCE;
public void doSomething() {
System.out.println("doSomething");
}
}
public class EnumSingleton {
public static void main(String[] args) {
SingletonEnum.INSTANCE.doSomething();
}
}
直接通过 SingletonEnum.INSTANCE.doSomething() 的方式调用即可。方便、简洁又安全。