设计模式02-单例模式

单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。单例模式属于创建型模式。它提供了一种创建对象的方式,确保只有单个对象被创建,从而节约资源、节约时间。

示例代码

饿汉式单例——在单例类首次加载时就创建实例

简单代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class HungrySingleton {
//首次加载时就创建实例,使用final关键字防止被覆盖
private static final HungrySingleton hungrySingleton = new HungrySingleton();

//私有的构造器
private HungrySingleton() {
}

//公共的全局访问点
public static HungrySingleton getInstance() {
return hungrySingleton;
}
}

静态代码块实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class HungryStaticSingleton {
//首次加载时就创建实例
private static final HungryStaticSingleton hungrySingleton;

static {
hungrySingleton = new HungryStaticSingleton();
}

//私有的构造器
private HungryStaticSingleton() {
}

//公共的全局访问点
public static HungryStaticSingleton getInstance() {
return hungrySingleton;
}
}

饿汉式单例在类加载的时候就立即初始化,并且创建单例对象。在线程未出现之前就已经实例化了,不可能存在访问安全问题,绝对线程安全。优点:没有加任何锁,执行效率高,比懒汉式单例更好;缺点:类加载的时候就初始化,不管后续用没用上都预占了资源,可能造成内存浪费。

为了解决资源浪费问题,出现了性能更优的懒汉式单例

懒汉式单例——被外部类调用时才创建实例

简单代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class LazySimpleSingleton {

private static LazySimpleSingleton lazy = null;

private LazySimpleSingleton() {
}

//线程不安全
public static LazySimpleSingleton getInstance() {
//“懒”在被调用时才会实例化
if (lazy == null) {
lazy = new LazySimpleSingleton();
}
return lazy;
}
}

懒汉式单例的简单代码实现通过被调用时再实例化解决了预占资源的问题,但是同时引入了线程安全问题

测试线程类ExecutorThread

反射破坏单例

至此,上述单例模式的构造方法中除了加上private外,未做其它处理。如果使用反射来调用其构造方法,然后再调用getInstance()应该会出现两个不同实例,以LazyInnerClassSingleton为例,测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class LazyInnerClassSingletonTest {
public static void main(String[] args) {
//利用反射强行破坏懒汉式单例
try {
Class<?> clazz = LazyInnerClassSingleton.class;
Constructor constructor = clazz.getDeclaredConstructor(null);
constructor.setAccessible(true);
Object o1 = constructor.newInstance();
Object o2 = constructor.newInstance();
System.out.println(o1 == o2);
} catch (Exception e) {
e.printStackTrace();
}
}
}

运行后可以看到输出结果:false ,很明显创建了两个不同实例。

将构造方法中添加限制,一旦出现重复创建则直接抛出异常,优化实现后如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class LazyInnerClassSingleton {
private LazyInnerClassSingleton() {
if (LazyHolder.LAZY != null) {
throw new RuntimeException("不允许构建多个实例!");
}
}

//static为了使单例的空间共享
//final保证此方法不会被重写、重载
public static final LazyInnerClassSingleton getInstance() {
//返回结果前一定会先加载内部类
return LazyHolder.LAZY;
}

private static class LazyHolder {
private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
}
}

再次运行测试类可以看到输出结果:

序列化破坏单例

将一个单例对象创建好后,有时需要将对象序列化写入到磁盘,下次使用时再从磁盘中读取到对象,反序列化转为内存对象。反序列化的对象会重新分配内存,即重新创建,如果序列化的目标为单例对象,那就违背了单例模式的初衷,相当于破坏了单例。测试代码如下:

1
2
3
4
5
6
7
8
9
10
public class SeriableSingleton implements Serializable {
private static final SeriableSingleton INSTANCE = new SeriableSingleton();

private SeriableSingleton() {
}

public static SeriableSingleton getInstance() {
return INSTANCE;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//利用序列化破坏饿汉式单例
public class SeriableSingletonTest {
public static void main(String[] args) {

SeriableSingleton s1 = null;
SeriableSingleton s2 = SeriableSingleton.getInstance();

FileOutputStream fos = null;
try {
fos = new FileOutputStream("SeriableSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s2);
oos.flush();
oos.close();

FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (SeriableSingleton) ois.readObject();
ois.close();

System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);
} catch (Exception e) {
e.printStackTrace();
}
}
}

运行结果:

1
2
3
com.example.designpattern.singleton.hungry.SeriableSingleton@1794d431
com.example.designpattern.singleton.hungry.SeriableSingleton@3e6fa38a
false

可以看到,反序列化后的对象和手动创建的对象是不一致的,实例化了两次,违背了单例设计的初衷。可以通过增加readResolve()来保证序列化的情况下也能够实现单例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SeriableSingleton implements Serializable {
private static final SeriableSingleton INSTANCE = new SeriableSingleton();

private SeriableSingleton() {
}

public static SeriableSingleton getInstance() {
return INSTANCE;
}

//重写readResolve方法覆盖反序列化出来的对象;仍然创建了两次,在JVM层面相对比较安全
//之前反序列化出来的对象会被GC回收
private Object readResolve() {
return INSTANCE;
}
}

再次运行测试类可以看到:

1
2
3
com.example.designpattern.singleton.hungry.SeriableSingleton@3e6fa38a
com.example.designpattern.singleton.hungry.SeriableSingleton@3e6fa38a
true

具体原理可查看JDK源码:ObjectInputStream类的readObject()方法。通过分析源码及断点调试可以看到单例类实际上被实例化了两次,只不过新创建的对象没有返回;如果创建对象的频率增加,那就意味着内存开销会增加,为了解决这个问题,出现了注册式单例。

注册式单例——将每一个实例都缓存到统一的容器中,使用唯一标识获取实例

注册式单例又称为登记式单例,就是将每一个实例都登记到某一个地方,使用唯一的标识获取实例。注册式单例有两种写法:第一种是枚举式登记,第二种是容器缓存。

枚举式单例测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public enum EnumSingleton {

INSTANCE;

//测试使用
private Object object;

public Object getObject() {
return object;
}

public void setObject(Object object) {
this.object = object;
}

public static final EnumSingleton getInstance() {
return INSTANCE;
}
}

测试方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class EnumSingletonTest {
public static void main(String[] args) {
try {
EnumSingleton instance1 = null;
EnumSingleton instance2 = EnumSingleton.getInstance();
instance2.setObject(new Object());

FileOutputStream fos = new FileOutputStream("EnumSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(instance2);
oos.flush();
oos.close();

FileInputStream fis = new FileInputStream("EnumSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
instance1 = (EnumSingleton) ois.readObject();
ois.close();

System.out.println(instance1.getObject());
System.out.println(instance2.getObject());
System.out.println(instance1.getObject() == instance2.getObject());
} catch (Exception e) {
e.printStackTrace();
}
}
}

运行结果:

1
2
3
java.lang.Object@548a9f61
java.lang.Object@548a9f61
true

没有做任何特殊处理,可见枚举式单例可以防止序列化破坏单例。原因在于枚举式单例在静态代码块中就给INSTANCE进行了赋值,是饿汉式单例的体现。反编译class文件后可以看到如下内容:

1
2
3
4
5
6
7
static 
{
INSTANCE = new EnumSingleton("INSTANCE", 0);
$VALUES = (new EnumSingleton[] {
INSTANCE
});
}

查看JDK源码可以发现枚举类型是通过类名和Class对象找到唯一的枚举对象,因此,枚举对象不可能被类加载器加载多次。序列化没问题了,那么反射如何?

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class EnumSingletonTest {
public static void main(String[] args) {
try {
Class<?> clazz = EnumSingleton.class;
//不传参时会报错java.lang.NoSuchMethodException
//从反编译出的类文件中可以看到没有无参构造器,只有一个包含参数String和int的构造器
Constructor constructor = clazz.getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true);
//通过源码可以看到jdk层面已经防止枚举被反射破坏
EnumSingleton obj = (EnumSingleton) constructor.newInstance("amazing", int.class);
System.out.println(obj);
} catch (Exception e) {
e.printStackTrace();
}

}

}

运行结果:

1
2
3
4
java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:484)
at com.example.designpattern.singleton.register.EnumSingletonTest.main(EnumSingletonTest.java:47)

查看JDK源码可以看到在Constructor类的newInstance()方法中做了强制性的判断,如果修饰符是Modifier.ENUM类型会直接抛出异常。枚举式单例是一种比较优雅的写法,在《Effective Java》中也推荐这种写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
if (!override) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, clazz, modifiers);
}
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}

接下来再看容器缓存式单例,测试类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ContainerSingleton {

private ContainerSingleton() {
}

private static Map<String, Object> ioc = new ConcurrentHashMap<>();

public static Object getBean(String className) {
if (!ioc.containsKey(className)) {
Object obj = null;
try {
obj = Class.forName(className).getDeclaredConstructor().newInstance();
ioc.put(className, obj);
} catch (Exception e) {
e.printStackTrace();
}
return obj;
}
return ioc.get(className);
}
}

容器式单例适用于创建实例非常多的情况,便于管理,但它是线程不安全的。

ThreadLocal线程单例——保证线程内部的全局唯一,且天生线程安全

ThreadLocal不能保证其创建的对象是全局唯一,但是能保证在单个线程中是唯一的。

1
2
3
4
5
6
7
8
9
10
11
12
13
//伪线程安全:线程内部是线程安全的,线程之外是不安全的
public class ThreadLocalSingleton {

private ThreadLocalSingleton() {
}

private static ThreadLocal<ThreadLocalSingleton> threadLocalInstance =
ThreadLocal.withInitial(ThreadLocalSingleton::new);

public static ThreadLocalSingleton getInstance() {
return threadLocalInstance.get();
}
}

ExecutorThread.java

1
2
3
4
5
6
7
public class ExecutorThread implements Runnable {
@Override
public void run() {
ThreadLocalSingleton singleton = ThreadLocalSingleton.getInstance();
System.out.println(Thread.currentThread().getName() + ": " + singleton);
}
}

测试方法类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ThreadLocalSingletonTest {
public static void main(String[] args) {
System.out.println(Thread.currentThread());
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(ThreadLocalSingleton.getInstance());

Thread t1 = new Thread(new ExecutorThread());
Thread t2 = new Thread(new ExecutorThread());

t1.start();
t2.start();

System.out.println("end");
}
}

运行结果:

1
2
3
4
5
6
7
Thread[main,5,main]
com.example.designpattern.singleton.threadlocal.ThreadLocalSingleton@1efbd816
com.example.designpattern.singleton.threadlocal.ThreadLocalSingleton@1efbd816
com.example.designpattern.singleton.threadlocal.ThreadLocalSingleton@1efbd816
end
Thread-0: com.example.designpattern.singleton.threadlocal.ThreadLocalSingleton@3ca8ab72
Thread-1: com.example.designpattern.singleton.threadlocal.ThreadLocalSingleton@107ed120

在主线程中,无论调用多少次,获取到的实例都是同一个,而在两个子线程中分别获取到了不同的实例。前述的单例实现为了达到线程安全的目的,会给方法上锁,以时间换空间;ThreadLocal将所有的对象放在ThreadLocalMap中,为每个线程都提供一个对象,实际上是以空间换时间来实现线程间隔离的。

Spring实例

  • ServletContext
  • ServletConfig
  • ApplicationContext
  • DBPool

适用场景

  • 确保在任何情况下都绝对只有一个实例

模式优点

  • 在内存中只有一个实例,减少了内存开销
  • 可以避免对资源的多重占用
  • 设置全局访问点,严格控制访问

模式缺点

  • 没有接口,扩展困难
  • 如果要扩展对象,只有修改代码,没有其它途径

总结

使用单例时需要考虑的问题:

  1. 程序性能
  2. 线程安全
  3. 序列化/反序列化破坏单例
  4. 反射破坏单例
  5. 深度克隆破坏单例
单例模式 多线程 反射 指令重排 反序列化
经典饿汉式
经典懒汉式
枚举式单例
容器式单例
ThreadLocal单例

综上,平常开发中考虑使用枚举式单例,其它情况根据实际业务场景选择合适的实现方式。

1
2
3
4
5
6
7
8
public enum EnumSingleton {

INSTANCE;

public static final EnumSingleton getInstance() {
return INSTANCE;
}
}

参考