定义:
保证一个类仅有一个实例, 并提供一个全局访问点
类型:
创建型
使用场景
- 确保任何情况下都绝对只有一个实例
coding
单例模式需要注意的点
- 私有构造器
- 线程安全
- 延迟加载
- 序列化和反序列化安全
- 防止反射机制破坏单例模式
单例模式的N种写法
1. 饿汉式
- 实现简单
- 线程安全
public class HungrySingleton {
private static HungrySingleton INSTANCE = new HungrySingleton();
private HungrySingleton(){}
public static HungrySingleton getInstance() {
return INSTANCE;
}
}
初始化类时就加载, 如果不使用就会浪费内存
2. 懒汉式
- 实现简单
- 延迟加载
public class LazySingleton {
private static LazySingleton INSTANCE;
private LazySingleton(){}
public static LazySingleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new LazySingleton();
}
return INSTANCE;
}
}
懒汉式的优点是延迟加载,等到需要的时候才会创建实例, 但他是线程不安全的, 当两个线程同时进入getInstance方法时, 线程1和2都执行到
INSTANCE == null, 此时INSTANCE如果还未创建, 将会创建两个实例线程不安全可以通过多线程调试来复现
IDEA多线程调试
3. 懒汉式 + 同步锁
- 延迟加载
- 线程安全
public class LazySyncSingleton {
private static LazySyncSingleton INSTANCE;
private LazySyncSingleton(){}
public static synchronized LazySyncSingleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new LazySyncSingleton();
}
return INSTANCE;
}
}
这样子线程就安全了,但是消耗了不必要的同步资源,不推荐这样使用。
4. DCL模式(Double CheckLock) - 双重检查
- 延迟加载
- 线程安全
- 相对懒汉式 + 同步锁的方式只在初始化时才会加锁, 提高了效率
public class LazyDoubleCheckSingleton {
private static LazyDoubleCheckSingleton INSTANCE;
private LazyDoubleCheckSingleton(){}
public static synchronized LazyDoubleCheckSingleton getInstance() {
if (INSTANCE == null) {
synchronized (LazyDoubleCheckSingleton.class) {
if (INSTANCE == null) {
INSTANCE = new LazyDoubleCheckSingleton();
}
}
}
return INSTANCE;
}
}
通过两个判断,第一层是避免不必要的同步,第二层判断是否为null。
可能会出现DCL模式失效的情况。
DCL模式失效:
singleton=new Singleton()
这句话执行的时候,会进行下列三个过程:
- 分配内存。
- 初始化构造函数和成员变量。
- 将对象指向分配的空间。
由于JMM(Java Memory Model)的规定,可能会对单线程情况下不影响程序运行结果的指令进行重排序, 因此可能会出现1-2-3和1-3-2两种情况。
所以,就会出现线程A进行到1-3时,就被线程B取走,此时B线程拿到的是一个还未初始化完成的对象, 这时就出现了异常, DCL模式就失效了。
可以使用 volatile 来解决重排序问题
volatile 有禁止指令重排序的功能. volatile详解
private volatile static LazyDoubleCheckSingleton INSTANCE;
5.内部类实现单例
- 线程安全
- 实现简单
- 延迟加载
public class StaticInnerClassSingleton {
private static class SingletonHolder {
private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
}
private StaticInnerClassSingleton(){}
public static StaticInnerClassSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
利用了class的初始化锁保证只有一个线程能加载内部类
只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance (只有拿到初始化锁的线程才会初始化对象)
6.枚举
- 实现简单
- 线程安全
- 避免反序列化破坏单例
- 避免反射攻击
public enum EnumSingleton {
INSTANCE(new Object());
EnumSingleton(Object data) {
this.data = data;
}
/**
* 单例实体
*/
private Object data;
public Object getData() {
return data;
}
public static EnumSingleton getInstance(){
return INSTANCE;
}
}
Spring管理单例bean就是容器管理
7.容器
- 统一管理单例
public class ContainerSingleton {
private ContainerSingleton(){}
private static Map CONTAINER = new HashMap();
public static void putInstance(String key,Object instance){
if(StringUtils.isNotBlank(key) && instance != null){
if(!CONTAINER.containsKey(key)){
CONTAINER.put(key,instance);
}
}
}
public static Object getInstance(String key){
return CONTAINER.get(key);
}
}
8.特殊的单例模式 ThreadLocal 实现线程单例
public class ThreadLocalInstance {
private static final ThreadLocal INSTANCE
= ThreadLocal.withInitial(ThreadLocalInstance::new);
private ThreadLocalInstance(){
System.out.println("init");
}
public static ThreadLocalInstance getInstance(){
return INSTANCE.get();
}
}
测试
public class ThreadLocalInstance {
private static final ThreadLocal INSTANCE
= ThreadLocal.withInitial(ThreadLocalInstance::new);
private ThreadLocalInstance(){
System.out.println("init");
}
public static ThreadLocalInstance getInstance(){
return INSTANCE.get();
}
}
运行, 在输出结果中可以看到, 用一个线程获取到的实例都是相同的, 即每个线程中只有一个实例存在, 在很多情况下是非常有用的,篇幅原因就不详细展开了,想详细了解的可以看一下这边文章 => Java并发编程:深入剖析ThreadLocal
源码中的单例
单例在源码中是广泛使用的
比如常用的工具类 java.lang.Math#random
方法
public static double random() {
return RandomNumberGeneratorHolder.randomNumberGenerator.nextDouble();
}
这个RandomNumberGeneratorHolder.randomNumberGenerator
是什么呢?
private static final class RandomNumberGeneratorHolder {
static final Random randomNumberGenerator = new Random();
}
这正是上面提到的内部类实现单例的模式.
其他的比如java.lang.Runtime
中
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
一个非常明显的饿汉式单例 等
序列化对单例模式的破坏
java中提供了对象的序列化与反序列化功能, 对象实现了Serializable接口之后就可以对对象的实例进行序列化与反序列化, 下面以HungrySingleton 为例看一下 反序列化破坏单例模式的实例
public class SerializationBrokenSingletonTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
HungrySingleton instance = HungrySingleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
// 序列化
oos.writeObject(instance);
File file = new File("singleton_file");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
// 反序列化
HungrySingleton newInstance = (HungrySingleton) ois.readObject();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
}
运行结果:
com.hhx.design.pattern.creational.singleton.HungrySingleton@135fbaa4
com.hhx.design.pattern.creational.singleton.HungrySingleton@568db2f2
false
明显看到 反序列化之后, 得到了一个不同的对象实例.
在HungrySingleton中添加readResolve()方法
private Object readResolve(){
return hungrySingleton;
}
再次运行代码
com.hhx.design.pattern.creational.singleton.HungrySingleton@135fbaa4
com.hhx.design.pattern.creational.singleton.HungrySingleton@135fbaa4
true
神奇的发现返回true, 这是怎么回事呢,
有兴趣的朋友可以debug跟踪一下
ObjectInputStream#readObject
ObjectInputStream#readObject0
ObjectInputStream#readOrdinaryObject
重点关注
ObjectInputStream#readOrdinaryObject
中的
obj = desc.isInstantiable() ? desc.newInstance() : null
与
Object rep = desc.invokeReadResolve(obj)
就可以知道 readResolve 的调用原理了.
反射对单例模式的破坏
public class ReflectBrokenSingletonTest {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
HungrySingleton instance = HungrySingleton.getInstance();
Class clazz = HungrySingleton.class;
Constructor constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
HungrySingleton newInstance = constructor.newInstance();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
}
- 对于在类加载阶段就初始化的实例的单例模式(饿汉式, 内部类)可以通过在构造器中抛出异常的方式防止反射攻击
private HungrySingleton(){
if(INSTANCE != null){
throw new RuntimeException("单例构造器禁止反射调用");
}
}
对于懒加载的单例模式(懒汉式, 懒汉式+同步锁, DCL模式), 如果在构造器中抛出异常的话, 当实例在反射调用constructor.newInstance()执行之前就已经实例化时, 是可以按照预期抛出异常的, 但是如果单例模式中的实例还未被实例化, 执行constructor.newInstance()不会抛出异常, 因为此时INSTANCE == null.
优点:
- 内存只有一个实例, 减少内存开销
- 设置全局访问点, 严格控制访问
缺点:
- 没有接口, 扩展困难
github源码