大家好我是miHotel,今天来复习一下设计模式中的单例模式,下面是思维导图。
请添加图片描述

什么是单例模式

单例模式顾名思义,就是一个类从始至终只能创建一个对象,并且提供了一个全局访问点。ServletContext、ServletContextConfig、ApplicationContext、数据库连接池都是单例模式。

饿汉单例模式

首先是饿汉单例模式,所谓的“饿汉”就是不采用延迟加载,在类加载的时候就初始化,并创建单例对象。饿汉单例模式是绝对线程安全的,因为在线程还没有出现的时候就初始化了。
第一种写法:

1
2
3
4
5
6
7
8
9
public class HungrySingleton {
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
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
public class LazySimpleSingleton {
private static LazySimpleSingleton lazySimpleSingleton = null;
private LazySimpleSingleton() {}
public static LazySimpleSingleton getInstance() {
if (lazySimpleSingleton == null) {
lazySimpleSingleton = new LazySimpleSingleton();
}
return lazySimpleSingleton;
}
}

这种写法存在的问题就是线程不安全,当多个线程同时访问getInstance方法时,有可能同时进入if语句,从而创建了多个对象,后创建的对象会覆盖第一个对象。通过加锁可以解决线程不安全问题:

1
2
3
4
5
6
7
8
9
10
public class LazySimpleSingleton {
private static LazySimpleSingleton lazySimpleSingleton = null;
private LazySimpleSingleton() {}
public synchronized static LazySimpleSingleton getInstance() {
if (lazySimpleSingleton == null) {
lazySimpleSingleton = new LazySimpleSingleton();
}
return lazySimpleSingleton;
}
}

给getInstance整个方法进行加锁,这样当一个线程进入该方法时另外一个线程会从RUNNING状态变成MONITOR状态,第一个线程执行完成后,第二个线程再进入getInstance方法后lazySimpleSingleton≠null,因此只会创建一个对象。这种写法虽然解决了线程安全问题,但是性能差,当有大量线程访问是会照成很多线程发生阻塞,因此需要对代码进行改进,下面是双检的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton lazy;
private LazyDoubleCheckSingleton() {}
public static LazyDoubleCheckSingleton getInstance() {
if (lazy == null) {
synchronized (LazyDoubleCheckSingleton.class) {
if (lazy == null) {
lazy = new LazyDoubleCheckSingleton();
}
}
}
return lazy;
}
}

通过改进,多个线程都可以进入getInstance方法,也可以通过第一层检查,然后就会竞争一个锁,第一个拿到锁的线程会对lazy进行实例化,后面拿到锁的线程就不会通过第二层检查。这里给静态成员变量加上volatile关键字是防止编译器对lazy = new LazyDoubleCheckSingleton()这段代码的指令进行优化,这段代码的指令分为三条:

  1. 分配内存空间
  2. 初始化对象
  3. 将变量lazy指向刚刚分配的内存空间(这步后lazy不为null)

volatile可以防止第二条和第三条指令发生重排序,指令顺序变为1->3->2,也就是lazy不为null但是没有初始化。在 3 执行完毕、2 未执行之前,被另一个抢占了,这时 lazy已经是非 null 了(但却没有初始化),所以该线程会直接返回 lazy,然后使用,然后顺理成章地报空指针。

双检的写法毕竟是需要加锁的,所以还是会对性能产生影响。利用类的加载性质,可以使用静态内部类实现懒汉单例模式,首先我们知道一个类的初始化时机有以下6种:

  • (1) 创建类的实例( new)
  • (2) 访问某个类或接口的静态变量,或对静态变量赋值
  • (3) 调用类的静态方法
  • (4) 反射 (Class.forname(“全限定类名”))
  • (5) 初始化一个类的子类(先初始化父类)
  • (6) JVM启动时标明的启动类,就是类名和文件名相同的那个类 注意: 访问常量(static final)不会导致类的初始化; 使用Class.loader()方法加载类时也不会对类初始化

可以看到访问某个类的静态的静态变量这个类就会被初始化,类的加载过程不会马上加载内部类,而是在使用时进行加载,利用这个性质就能实现懒加载,而JVM已经保证了线程的安全性,静态内部类在方法调用前就会被初始化,代码如下:

1
2
3
4
5
6
7
8
9
public class LazyInnerClassSingleton {
private LazyInnerClassSingleton() {}
public static LazyInnerClassSingleton getInstance() {
return LazyHolder.LAZY;
}
private static class LazyHolder {
private static LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
}
}

反射破坏单例模式

上面代码的构造器除了加了private修饰符以外没有做任何限制,因此可以通过反射来破坏单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void test04() {
try {
Class<LazyInnerClassSingleton> clazz = LazyInnerClassSingleton.class;
Constructor<LazyInnerClassSingleton> c = clazz.getDeclaredConstructor(null);
c.setAccessible(true);// 强制访问
LazyInnerClassSingleton o1 = c.newInstance();
LazyInnerClassSingleton o2 = c.newInstance();
System.out.println(o1 == o2);
}catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}

输出:false, 因此需要对构造器进行处理,代码如下:

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

private LazyInnerClassSingleton2() {
if (LazyHolder.LAZY != null) {
throw new RuntimeException("不允许创建多个实例");
}
}
public static LazyInnerClassSingleton2 getInstance() {
return LazyHolder.LAZY;
}
public static class LazyHolder {
public static LazyInnerClassSingleton2 LAZY = new LazyInnerClassSingleton2();
}
}

通过反射调用构造器创建对象时,执行LazyHolder.LAZY语句时,静态内部类会初始化,导致LazyHolder.LAZY != null,从而抛出异常,无法创建多个对象。

序列化破坏单例对象

序列化是指将一个对象序列化后写入磁盘,下次使用时再从磁盘种读取对象并进行反序列化,转化为内存对象。反序列化的对象会重新分配内存,因此如果反序列化的目标对象是单例对象就会破坏单例,下面来看代码:
首先写一个饿汉单例模式:

1
2
3
4
5
6
7
8
9
10
public class SeriableSingleton implements Serializable {

private static 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
@Test
public void test06() {
try {
SeriableSingleton s1 = null;
SeriableSingleton s2 = SeriableSingleton.getInstance();
FileOutputStream fos = new FileOutputStream("SeriableSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s2);
oos.flush();
oos.close();

FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
ObjectInputStream ooi = new ObjectInputStream(fis);
s1 = (SeriableSingleton) ooi.readObject();
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);
} catch (Exception e) {
e.printStackTrace();
}
}

控制台输出如图所示:
在这里插入图片描述
可以看到反序列化后的对象和手动创建的对象是不一样的,实例化了两次,也就破坏了单例。那么如何解决这个问题呢?其实我们只需要增加readResolve方法即可,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class SeriableSingleton implements Serializable {

private static SeriableSingleton instance = new SeriableSingleton();

private SeriableSingleton() {}

public static SeriableSingleton getInstance() {
return instance;
}

private Object readResolve() {
return instance;
}
}

再来测试一下,看看运行结果,如下图所示。
在这里插入图片描述
可以看到神奇的事情发生了, 加上了readResolve方法后反序列化并没有破坏单例模式,那这到底是什么原因呢,我们来看看ObjectInputStream类的readObject方法:

1
2
3
4
5
6
7
8
9
10
11
private final Object readObject(Class<?> type)
throws IOException, ClassNotFoundException
{
....
try {
Object obj = readObject0(type, false);
.....
return obj;
}
.......
}

再进入readObject0方法

1
2
3
4
5
6
7
8
9
private Object readObject0(Class<?> type, boolean unshared) throws IOException {
...
switch (tc) {
case TC_OBJECT:
....
return checkResolve(readOrdinaryObject(unshared));
}
...
}

可以看到在TC_OBJECT中调用了readOrdinaryObject方法, 我们再进入该方法:

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
private Object readOrdinaryObject(boolean unshared)
throws IOException {
...
Object obj;
try {
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
...
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
.....
if (rep != obj) {
.....
handles.setObject(passHandle, obj = rep);
}
}
...
return obj;
}

通过阅读源码,我们可以知道,isInstantiable()是判断构造器是否为空,如果不为空就返回true,因此就会创建一个新的对象。再往下看我们可以看到还调用了hasReadResolveMethod方法来判断反序列化的对象有没有readResolve方法,如果有则返回true,然后就会invokeReadResolve方法,看这方法名就知道是通过反射来调用readResolve方法,我们进入该方法看看源码:

1
2
3
4
5
6
7
Object invokeReadResolve(Object obj)
throws IOException, UnsupportedOperationException
{
....
return readResolveMethod.invoke(obj, (Object[]) null);
....
}

到这是不是有点恍然大悟,rep就是调用我们添加的readResolve方法返回的单例对象,源码中做了一个判断,如果创建的obj对象与rep对象不同,则将rep赋值给obj,最后返回,这样反序列化返回的就是单例对象。通过源码分析可以看出readResolve虽然解决了单例模式被破坏的问题,但是实际上该对象还是创建了两次,只是最后返回了相同的引用,如果创建对象的频率加快,内存开销也会增大。

注册式单例模式

注册式单例模式顾名思义就是将每个实例用唯一标识符登记再某个“本子”上,如果要使用某个实例时,就去”本子“上查。注册式单例模式可以分为枚举式和容器式。

1.枚举式单例模式

首先我们来看一下枚举式单例模式的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public enum EnumSingleton {
INSTANCE;
private Object data;

public Object getData() {
return data;
}

public void setData(Object data) {
this.data = data;
}

public static 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
@Test
public void test07() {
try {
EnumSingleton instance1 = null;
EnumSingleton instance2 = EnumSingleton.getInstance();
instance2.setData(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 ooi = new ObjectInputStream(fis);
instance1 = (EnumSingleton) ooi.readObject();
System.out.println(instance1);
System.out.println(instance2);
System.out.println(instance1 == instance2);
} catch (Exception e) {
e.printStackTrace();
}
}

运行结果如图所示
在这里插入图片描述

为啥枚举式单例模式不会被反序列化破坏呢,我们使用反编译工具jad来看一下EnumSingleton.class的反编译代码, 可以看到如下代码

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

这说明枚举型单例模式是饿汉模式的实现,我们再看下JDK源码,搞清楚反序列化为啥不能破坏枚举式单例模式。下面是java.io.ObjectInputStream#readObject0的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
private Object readObject0(Class<?> type, boolean unshared) throws IOException {
....
switch (tc) {
....
case TC_ENUM:
if (type == String.class) {
throw new ClassCastException("Cannot cast an enum to java.lang.String");
}
return checkResolve(readEnum(unshared));
....
}
....
}

再进入readEnum方法

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
29
30
31
32
33
34
35
36
37
38
private Enum<?> readEnum(boolean unshared) throws IOException {
if (bin.readByte() != TC_ENUM) {
throw new InternalError();
}

ObjectStreamClass desc = readClassDesc(false);
if (!desc.isEnum()) {
throw new InvalidClassException("non-enum class: " + desc);
}

int enumHandle = handles.assign(unshared ? unsharedMarker : null);
ClassNotFoundException resolveEx = desc.getResolveException();
if (resolveEx != null) {
handles.markException(enumHandle, resolveEx);
}

String name = readString(false);
Enum<?> result = null;
Class<?> cl = desc.forClass();
if (cl != null) {
try {
@SuppressWarnings("unchecked")
Enum<?> en = Enum.valueOf((Class)cl, name);
result = en;
} catch (IllegalArgumentException ex) {
throw (IOException) new InvalidObjectException(
"enum constant " + name + " does not exist in " +
cl).initCause(ex);
}
if (!unshared) {
handles.setObject(enumHandle, result);
}
}

handles.finish(enumHandle);
passHandle = enumHandle;
return result;
}

可以发现,反序列化的枚举类型是通过类名和类对象来找到唯一的枚举对象,因此枚举对象不会被类加载器加载多次。再试试反射能否破坏单例模式, 首先java.lang.Enum只有一个构造器:

1
2
3
4
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}

我们尝试通过反射来调用这个构造器,创建对象

1
2
3
4
5
6
7
8
9
10
11
@Test
public void test08() {
try {
Class<EnumSingleton> clazz = EnumSingleton.class;
Constructor<EnumSingleton> c = clazz.getDeclaredConstructor(String.class, int.class);
c.setAccessible(true);
EnumSingleton miHoltel = c.newInstance("miHoltel", "666");
} catch (Exception e) {
e.printStackTrace();
}
}

运行结果如下图所示:

在这里插入图片描述

错误提示不能通过反射创建枚举对象,我们再来看看JDK源码,进入Contructor类的newInstance方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, null, 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;
}

可以看到在newInstance方法中做了强制判断,如果是枚举类型则直接抛出异常。

容器式单例模式

下面来看一下注册式单例模式的另外一种写法

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

private ContainerSingleton() {}

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

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

容器式单例模式适用于实例非常多的情况。Spring中应用容器式单例模式的代码如下:

1
2
3
4
5
6
7
8
9
10
11
public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory implements AutowireCapableBeanFactory {
.....
private final ConcurrentMap<String, BeanWrapper> factoryBeanInstanceCache;

public AbstractAutowireCapableBeanFactory() {
...
this.factoryMethodCandidateCache = new ConcurrentHashMap();
...
}
.....
}