确保一个类在任何情况下都只有一个实例被返回,并提供一个全局访问点。
隐藏其所有构造方法,所有构造方法私有。属于创建型模式。
ServletContext、ServletConfig、ApplicationContext、DBPool
优点:
- 在内存中只有一个实例,减少内存开销。
- 避免对资源的多重占用。
- 设置了全局访问点,控制了访问。
缺点:没有接口,扩展困难,违背了开闭原则。
注意事项:
- 私有化构造器
- 保证线程安全
- 延迟加载
- 防止序列化和反序列化破坏单例
- 防御反射攻击破坏单例
1. 饿汉式单例
类加载时就已经被初始化。
优点:执行效率高,性能高,没有任何的锁。
缺点:在不使用的情况下会造成内存的浪费。
public class HungrySingleton {
// 类加载的时候已经初始化,会占用内存空间,如果不使用,就会浪费内存
private static HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton() {}
public HungrySingleton getInstance() {
return hungrySingleton;
}
}
2. 懒汉式单例
第一次调用时才初始化。
2.1 简单懒汉式 + synchronized
public class LazySingleton {
// 加volatile避免指令重排序问题
private volatile static LazySingleton lazySingleton;
private LazySingleton() {}
// synchronized加锁避免第一次访问时两个线程同时来创建实例
// 但是方法锁会影响性能,后期每次获取实例都只能一次一个线程访问
public synchronized LazySingleton getInstance() {
if (null == lazySingleton) {
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
优点:节省内存资源
缺点:加锁,会有性能问题
注意:如果不加synchronized关键字,会有线程安全问题,多个线程同时访问有两种运行结果:
a. 同一个实例:正常顺序执行,但是后者结果覆盖了前者,从而打印。所以看着打印结果实例相同,实际上是两个实例。
b. 不同实例:同时进入条件,按顺序执行返回
2.2 DCL懒汉式 + volatile
对懒汉式单例进行优化:Double Check Lock
优点:调用的时候才初始化,如果不使用不占用内存空间,避免造成浪费。性能提升了,线程安全了。
缺点:可读性差了,两个if判断,代码不够优雅。
public class LazyDoubleCheckSingleton {
// 加volatile避免指令重排序问题
private volatile static LazyDoubleCheckSingleton lazySingleton;
private LazyDoubleCheckSingleton() {}
// synchronized加锁避免第一次访问时两个线程同时来创建实例
public static LazyDoubleCheckSingleton getInstance() {
if (null == lazySingleton) {//检查是否需要阻塞进入锁
synchronized(LazyDoubleCheckSingleton.class) {
if (null == lazySingleton) {//检查是否已经创建实例
// 这里可能出现指令重排序问题,所以在声明地方加上volatile关键字
lazySingleton = new LazyDoubleCheckSingleton();
}
}
}
return lazySingleton;
}
}
2.3 懒汉静态内部类
通过内部类创建实例,然后调用内部类的静态变量获取实例。静态成员变量:在类加载的时候就会分配内存空间;静态内部类:在使用的时候才会分配空间。
优点:利用Java本身语法,通过内部类,性能高,避免内存浪费。
缺点:不优雅。
public class LazyStaticInnerClassSingleton {
private LazyStaticInnerClassSingleton() {
// 避免通过反射暴力获取对象实例
if (null != LazyHelper.INSTANCE) {
throw new RuntimeException("不允许通过反射获取实例");
}
}
public LazyStaticInnerClassSingleton getInstance() {
return LazyHelper.INSTANCE;
}
private static class LazyHelper {
private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
}
}
3. 注册式单例
将每一个实例都缓存到统一的容器中,然后通过唯一的标识来获取实例。
3.1 枚举式单例
枚举式单例在类加载时就已经将实例放在了一个Map类型的枚举常量字典enumConstantDirectory中
private volatile transient Map<String, T> enumConstantDirectory = null;
优点:简单优雅,线程安全,不会被反射破坏(JDK底层已经针对反射做了处理),
缺点:某些情况下可能造成内存浪费,声明的时候已经将实例放在了内存中。不适合在大量使用单例的情况下使用此方式。
public enum EnumSingleton {
INSTANCE;
public static final EnumSingleton getInstance(){
return INSTANCE;
}
}
3.2 容器式单例
针对以上枚举式单例的问题, Spring做出了改良,提出了容器式单例。
public class ContainerSingleton {
private ContainerSingleton(){}
private static Map<String, Object> ioc = new ConcurrentHashMap<String, Object>();
public static Object getInstance(String className) {
Object instance = null;
if (!ioc.containsKey(className)) {
// 通过DCL双重检查锁解决线程安全问题
synchronized (ioc) {
if (!ioc.containsKey(className)) {
try {
instance = Class.forName(className);
ioc.put(className, instance);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
}
return instance = ioc.get(className);
}
}
3.3 序列化问题
在序列化、反序列化过程中,也会破坏单例。
序列化、反序列化问题:首先得到一个类的instance实例,然后对其进行序列化存储到磁盘,然后再反序列化得到对象,比较反序列化后的对象地址和原始实例地址,发现内存地址不同,这就是序列化、反序列化破坏单例的问题。
解决:在单例中添加一个私有readResolve方法即可解决序列化反序列化问题。
private Object readResolve() {return INSTANCE;}
public class SerializableSingleton implements Serializable {
private static final SerializableSingleton INSTANCE = new SerializableSingleton();
private SerializableSingleton() {}
public static SerializableSingleton getInstance() {
return INSTANCE;
}
// 解决序列化、反序列化问题
private Object readResolve() {return INSTANCE;}
}
4. ThreadLocal单例
保证同一个线程内部全局唯一,且天生线程安全。不同线程中实例不同。
public class ThreadLocalSingleton {
private static final ThreadLocal<ThreadLocalSingleton> threadLocalSingleton = new ThreadLocal<ThreadLocalSingleton>() {
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}
};
private ThreadLocalSingleton(){}
public static ThreadLocalSingleton getInstance() {
return threadLocalSingleton.get();
}
}
面试题
如何选择用哪种单例?
如果程序不复杂,单例对象不多,推荐饿汉式单例
如果经常发生多线程并发情况,推荐静态内部类和枚举式单例
哪些情况会破坏单例?
- 多线程情况下线程安全问题
解决方案:DCL双重检查锁,静态内部类单例 - JVM指令重排序
解决方案:在单例对象上添加voilate关键字 - 深克隆破坏单例
解决方案:重写克隆方法,只返回当前实例 - 反射攻击破坏单例
解决方案:在构造方法检查单例对象,如果已创建则抛异常
将单例的实现方式改为枚举式单例 - 反序列化破坏单例
解决方案:重写readResolve方法,将返回值设置为单例