单例模式(Singleton Pattern)属于创建型模式,它提供了一种创建对象较好的方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时保证只有单个对象被创建。这个类提供了一种访问其唯一对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
- 一个类只能有一个实例
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他对象提供这一实例。
单例模式的目的:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决:一个全局使用的类频繁地创建与销毁。
使用场景:向想确保任何情况下都只有一个实例(线程池、数据库连接池等)
优点:在内存里只有一个实例,减少了内存开销、可以避免对资源的多重占用、设置全局 访问点,严格控制访问
缺点:没有接口、扩展困难
重点:私有构造器、线程安全、延迟加载、序列化和反序列化安全、反射
相关设计模式:单例模式和工厂模式、单例模式和享元模式
单例模式的集中实现方式:
- 懒汉式(线程不安全)
懒汉式单例模式,顾名思义,这种单例模式很懒,在初始化的时候不创建,而是做一个延迟加载。这种实现方式最大的问题是在多线程的场景下不安全,没有加 synchronized 锁 。
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton() {
}
public synchronized static LazySingleton getInstance() {
if(lazySingleton == null) {
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
- DoubleCheck双重检查实现懒汉式(线程安全)
上述延迟加载的模式在多线程的情况下是不安全的,为了解决多线程问题,可以采用DoubleCheck双重检查实现懒汉式。这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是效率很低。
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
private LazyDoubleCheckSingleton() {
}
public synchronized static LazyDoubleCheckSingleton getInstance() {
if(lazyDoubleCheckSingleton == null) {
synchronized (LazyDoubleCheckSingleton.class) {
if(lazyDoubleCheckSingleton == null) {
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
}
}
}
return lazyDoubleCheckSingleton;
}
}
- 静态内部类(线程安全)
基于类初始化的延迟加载,这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
public class StaticInnerClassSingleton {
// InnerClass类的对象的初始化锁被哪个线程拿到,那个线程负责实例化对象
private static class InnerClass {
private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance() {
return InnerClass.staticInnerClassSingleton;
}
private StaticInnerClassSingleton() {
}
}
- 饿汉式(线程安全)
在类加载的时候就完成类的实例化。这种方式比较常用,但容易产生垃圾对象。它没有加锁,执行效率会提高,但是类加载时就初始化,浪费内存。它基于 classloader 机制避免了多线程的同步问题。
public class HungrySingleton {
private final static HungrySingleton hungrySingleton;
static {
hungrySingleton = new HungrySingleton();
}
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return hungrySingleton;
}
}
- 序列化破坏单例模式原理解析及解决方案(以饿汉式为例)
在序列化和反序列化之后拿到的不是同一个对象。这是因为ObjectInputStream 对象的readObject()方法调用了readObject0()方法,而当ObjectInputStream 读入的是对象时,readObject0()方法中的 readOrdinaryObject(unshared)方法会调用isInstantiable()方法,isInstantiable()方法在类运行时被实例化时会返回true,然后会调用newInstance()方法,这样就通过反射又创建了一个对象,所以在序列化和反序列化之后拿到的不是同一个对象。
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);
// 结果为 false ,说明两者不是同一个对象,这就违背了单例设计模式
System.out.println(instance == newInstance);
oos.close();
ois.close();
解决方案
public class HungrySingleton implements Serializable {
private static final long serialVersionUID = 1L;
private final static HungrySingleton hungrySingleton;
static {
hungrySingleton = new HungrySingleton();
}
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return hungrySingleton;
}
public Object readResolve() {
return hungrySingleton;
}
}
- 反射攻击解决方案及原理分析(以饿汉式为例)
Class objectClass = HungrySingleton.class;
Constructor constructor = objectClass.getDeclaredConstructor();
constructor.setAccessible(true);
HungrySingleton instance = HungrySingleton.getInstance();
HungrySingleton newInstance = (HungrySingleton) constructor.newInstance();
System.out.println(instance);
System.out.println(newInstance);
// 结果为 false ,说明两者不是同一个对象,这就违背了单例设计模式
System.out.println(instance == newInstance);
解决方案
public class HungrySingleton {
private final static HungrySingleton hungrySingleton;
static {
hungrySingleton = new HungrySingleton();
}
private HungrySingleton() {
if(hungrySingleton != null) {
throw new RuntimeException("单例构造器禁止反射调用");
}
}
public static HungrySingleton getInstance() {
return hungrySingleton;
}
}
- Enum枚举单例(线程安全)
这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。
public enum EnumInstance {
INSTANCE;
private Object date;
public Object getDate() {
return date;
}
public void setDate(Object date) {
this.date = date;
}
public static EnumInstance getInstance() {
return INSTANCE;
}
}
-
容器单例模式(线程安全问题取决于具体的实现)
容器单例的思路是这样的,维护一个Map,把单例的实例都存放进该Map中,用的时候只从该Map中取,试图实现单例;但这种思路有局限性,Map中存的单例对象是可以被更新掉的,如果两次取的间隔,发生了单例对象的更新,就会取到2个不同的对象,破坏了单例性;应用场景:如果程序中单例类很多,可以考虑用一个容器管理起来;
public class ContainerSingleton {
private static Map<String, Object> singletonMap = new HashMap<String, Object>();
private ContainerSingleton() {
}
public static void putInstance(String key, Object instance) {
if(instance != null) {
if(!singletonMap.containsKey(key)) {
singletonMap.put(key, instance);
}
}
}
public static Object getInstance(String key) {
return singletonMap.get(key);
}
}
- ThreadLocal 线程“单例模式”
不是传统意义中的单例模式,不同线程之间拿到的对象可能不同,但是每个线程中对象都是单例的