源码学习系列:设计模式之单例模式

今天发生了一件非常突然的事情,突然得我措手不及没有做好任何心理准备。事起肯定有因,也怪我自己的粗心大意造成了今天的后果,所以今天也算是给了我一个非常深刻的教训吧。以上只是我个人发的几句牢骚,跟本文无关。本篇博客是周末两天就准备好了的,整理了下趁今天贴出来,后面我要调整调整,博客更新可能会停滞一段时间。吃一堑长一智,愿自己变得越来越好。2019年3月11日19:25:51

设计模式之单例模式

单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。单例模式是创建型模式。单例模式在现实生活中应用也非常广泛。
例如,国家主席、公司 CEO、部门经理等。在 J2EE 标准中,ServletContext、ServletContextConfig 等;在 Spring 框架应用中 ApplicationContext;数据库的连接池也都是单例形式。

本篇目录

  1. 饿汉式单例
  2. 懒汉式单例
  3. 反射破坏单例
  4. 序列化破坏单例
  5. 注册式单例之容器缓存
  6. 注册式单例之枚举单例
  7. ThreadLocal 线程单例

# 饿汉式单例

先看代码:

1
2
3
4
5
6
7
public class HungrySingleton {
private static final HungrySingleton HUNGRY_SINGLETON = new HungrySingleton();
private HungrySingleton() {}
public static HungrySingleton getInstance() {
return HUNGRY_SINGLETON;
}
}

代码非常简单,使用了static关键字,在类被加载的时候就进行实例化,并私有构造方法,提供一个静态方法返回实例的引用。这里的静态成员变量也可以使用静态代码块进行初始化,如:

1
2
3
static {
HUNGRY_SINGLETON = new HungryStaticSingleton();
}

这种方式简单有效,没有任何形式的锁,所以不存在效率问题。但缺点是不管类有没有被使用,只要被加载就回初始化,会造成资源浪费,当这样的单例非常多的是时候,是需要占用很大的内存空间的。


# 懒汉式单例

为了解决上面的的问题,懒汉式单例应时而生:

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

这里是在需要获取实例的时候才会进行初始化,方法内部做了一个 null 判断,保证只会被初始化一次,并且为了保证线程安全加了同步锁。

这样的好处是,只有在类真正需要调用的时候才会被初始化,但是因为加了同步锁,会导致在多线程环境下产生性能问题。需要继续优化,看代码:

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

getInstance() 的代码中,使用了双重检查,同步锁外层非空判断是为了提高性能,内层非空判断是为了线程安全,同时为了解决指令重排序问题,在静态成员变量前面加了 volatile关键字,关于 volatile 相关知识这里不做展开。

这段代码解决了实例化之后的多线程访问的性能问题,但是在实例化之前,还是可能会有多个线程进入到 synchronized 代码中,虽然只有一次,但是终究是不完美的。

再看看下面的代码:

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

顾名思义,这段代码使用了内部类的形式,利用了内部类一定要在调用前初始化的特点,且这个特性是JVM提供并保证的,我们不需要为其加任何锁,同时也避免了线程安全带的问题,这种方式避免了饿汉式的资源浪费,也兼顾了 synchronized 的性能问题,算是比较完美的解决方案。

但是,为什么要来但是呢,因为总有一些不走寻常路的程序员,比如我,喜欢来一些野路子,会破坏上面的代码的单例性质,接着看。


# 反射破坏单例

就一上面最后一步的代码来说,正常调用时没有任何问题的。但是如果我偏不走 getInstance() 方法,我用反射调用呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
try {
Class<?> clazz = LazyInnerClassSingleton.class;
// 通过反射拿到私有的构造方法
Constructor c = clazz.getDeclaredConstructor(null);
// 开启暴力访问,强吻,不愿意也要上
c.setAccessible(true);
// 暴力初始化
LazyInnerClassSingleton c1 = (LazyInnerClassSingleton) c.newInstance();
LazyInnerClassSingleton c2 = (LazyInnerClassSingleton) c.newInstance();
// 很明显这里的c1和c2肯定不是同一个对象
System.out.println(c1 == c2); // false
} catch (Exception e) {
e.printStackTrace();
}

不仅仅是上面最后一步的代码,上面其他步骤中的代码,用反射强制调用都会破坏单例性质,我 Java 大反射就是这么任性,就喜欢看你看不惯我又干不掉我的样子,哈哈。

你说,那我可以在私有构造方法中抛异常,比如:

1
2
3
4
5
private LazyInnerClassSingleton() {
if (LazyHolder.LAZY != null) {
throw new RuntimeException("不允许创建多个实例");
}
}

但这时又回到最开始的线程安全问题上了,多线程反射调用时,你要保证构造方法的线程安全,又要加锁,又要处理。等于又回到第一步第二部中的循环中去了。


# 序列化破坏单例

再看另一种情况,序列化。当我们将一个对象创建好后,有时候需要将对象写入磁盘或者通过网络流传输,需要先序列化,下次使用时,进行反序列化。但是反序列化之后的对象会重新分配内存,相当于重新创建。这种情况下也会破坏单例。以第一步饿汉式单例为例,看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
HungrySingleton s1 = null;
HungrySingleton s2 = HungrySingleton.getInstance();

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

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

System.out.println(s1); // com.guitu18.singleton.HungrySingleton@11531931
System.out.println(s2); // com.guitu18.singleton.HungrySingleton@7f31245a
System.out.println(s1 == s2); // false
} catch (Exception e) {
e.printStackTrace();
}

这里经过反序列化之后,返回的对象是一个新的对象,为什么会这样呢?

点进去 readObject() 一探究竟:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public final Object readObject() throws IOException, ClassNotFoundException {
if (enableOverride) {
return readObjectOverride();
}
// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
Object obj = readObject0(false);
handles.markDependency(outerHandle, passHandle);
ClassNotFoundException ex = handles.lookupException(passHandle);
...
return obj;
} finally {
...
}
}

为了便于阅读和保证页面简洁,省去了无关代码,下同。

可以看出在读取对象时时调用了 Object obj = readObject0(false) ,继续点进去

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
private Object readObject0(boolean unshared) throws IOException {
...
// 上面略去的部分代码是取得tc值,下面根据tc匹配类型选择不同的读取方式
try {
switch (tc) {
case TC_NULL:
return readNull();
case TC_REFERENCE:
return readHandle(unshared);
case TC_CLASS:
return readClass(unshared);
case TC_CLASSDESC:
case TC_PROXYCLASSDESC:
return readClassDesc(unshared);
case TC_STRING:
case TC_LONGSTRING:
return checkResolve(readString(unshared));
case TC_ARRAY:
return checkResolve(readArray(unshared));
case TC_ENUM:
return checkResolve(readEnum(unshared));
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
...
}
} finally {
depth--;
bin.setBlockDataMode(oldMode);
}
}

上面略去的部分代码是取得 tc 值,下面根据 tc 匹配类型选择不同的读取方式,这里走的是最后一个 TC_OBJECT 分支 checkResolve(readOrdinaryObject(unshared)) ,点进 readOrdinaryObject(unshared) 方法后会继续找到 resolveObject(obj) 方法,在这个方法里我找到了答案:

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
private Object readOrdinaryObject(boolean unshared) throws IOException {
...
ObjectStreamClass desc = readClassDesc(false);
desc.checkDeserialize();
Class<?> cl = desc.forClass();
if (cl == String.class || cl == Class.class || cl == ObjectStreamClass.class) {
throw new InvalidClassException("invalid class descriptor");
}
Object obj;
try {
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
...
}
...
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod()) {
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
// Filter the replacement object
if (rep != null) {
if (rep.getClass().isArray()) {
filterCheck(rep.getClass(), Array.getLength(rep));
} else {
filterCheck(rep.getClass(), -1);
}
}
handles.setObject(passHandle, obj = rep);
}
}
return obj;
}

在这里做了一个判断 desc.isInstantiable(),是否可以被序列化,判断依据也因简单,有没有构造方法。

这里判断为 true 走了 desc.newInstance() 返回了一个新的对象。这样也就解释了,单例为什么会被序列化破坏了。

那么就没有办法解决吗?有,我们只需要添加一个 readResolve 方法就可以了,还是以饿汉式为例,看代码:

1
2
3
4
5
6
7
8
9
10
11
public class HungrySingleton implements Serializable {
public final static HungrySingleton INSTANCE = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return INSTANCE;
}
private Object readResolve() {
return INSTANCE;
}
}

在最后添加了一个 readResolve 方法,返回了当前实例化出来的对象,这样就可以保证我们在经过序列化和反序列化之后还是原来的对象,保证了单例性质。

那么为什么会这样呢?继续看上面 readOrdinaryObject 方法代码最后的那个 if 判断 desc.hasReadResolveMethod()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod()) {
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
// Filter the replacement object
if (rep != null) {
if (rep.getClass().isArray()) {
filterCheck(rep.getClass(), Array.getLength(rep));
} else {
filterCheck(rep.getClass(), -1);
}
}
handles.setObject(passHandle, obj = rep);
}
}

这里很有趣,如果 desc.hasReadResolveMethod() 判断为 true 那么会调用一个 invokeReadResolve() 方法,如果 obj != rep 会将 rep 赋值给 obj返回。在desc.hasReadResolveMethod() 只做了一个判断:

1
2
3
4
boolean hasReadResolveMethod() {
requireInitialized();
return (readResolveMethod != null);
}

继续追踪这个成员变量 readResolveMethod,我们在代码中找到了为其赋值的地方:

1
readResolveMethod = getInheritableMethod(cl, "readResolve", null, Object.class);

我们继续查看 getInheritableMethod()这个方法做了什么:

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
private static Method getInheritableMethod(Class<?> cl, String name,
Class<?>[] argTypes,
Class<?> returnType) {
Method meth = null;
Class<?> defCl = cl;
while (defCl != null) {
try {
meth = defCl.getDeclaredMethod(name, argTypes);
break;
} catch (NoSuchMethodException ex) {
defCl = defCl.getSuperclass();
}
}

if ((meth == null) || (meth.getReturnType() != returnType)) {
return null;
}
meth.setAccessible(true);
int mods = meth.getModifiers();
if ((mods & (Modifier.STATIC | Modifier.ABSTRACT)) != 0) {
return null;
} else if ((mods & (Modifier.PUBLIC | Modifier.PROTECTED)) != 0) {
return meth;
} else if ((mods & Modifier.PRIVATE) != 0) {
return (cl == defCl) ? meth : null;
} else {
return packageEquals(cl, defCl) ? meth : null;
}
}

意思很明白,通过反射根据名称、形参和返回值在Class中查找方法,找到就将该方法返回并赋值给成员变量 readResolveMethod。根据调用时传入的参数,我们看出是在查找一个名为 readResolve,无形参,返回值为 Object 的方法,也就是说 readResolveMethod 保存的是我们自己添加的 readResolve() 方法。

再回到前面的 if 判断,如果该方法不为空,则调用该方法,并将返回值赋值给 obj 作为反序列化结果返回。

到这里谜团就揭开了,为什么添加了 readResolve 就能保证序列化不会破坏单例了,因为反序列化后拿到的就是我们添加的 readResolve 方法的返回值。

其实 JVM 在设计之初就考虑到了序列化会造成各种特殊情况,并对其做了一系列特殊处理。这里在反序列化时,虽然还是会 new 出来一个新的对象,但是在检查了 readResolve 方法后,会将该方法调用后的结果返回,之前 new 出来的对象会被垃圾回收器回收,这一切都是在 JVM 的保障下完成的。


# 注册式单例之容器缓存

注册式单例,也叫登记式单例。其实质就是维护一个 Map 来实现的,通过 key 来获取存放在 Map 中的实例。

大名鼎鼎的 SpringIOC 容器 BeanFactory 就是注册式单例。在调用时,先判断 Map 中是否已有该实例,有就直接返回,没有就创建一个保存到 Map 中再返回。

下面是一个 IOC 容器的简化版实现,代码比较简单,就不做说明和测试了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ContainerSingleton {
private static Map<String, Object> ioc = new ConcurrentHashMap<String, Object>();

private ContainerSingleton() {
}
public static Object getInstance(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);
}
}
}
}

# 注册式单例之枚举单例

再来看枚举式单例,枚举单例使用起来非常简单,代码如下:

1
2
3
4
5
6
7
8
9
10
public enum EnumSingleton {
INSTANCE;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}

直接上测试代码:

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
try {
EnumSingleton instance1 = EnumSingleton.getInstance();
instance1.setData(new Object());
EnumSingleton instance2 = EnumSingleton.getInstance();
System.out.println(instance1 == instance2); // true
System.out.println(instance1.getData()); // java.lang.Object@234bef66
System.out.println(instance2.getData()); // java.lang.Object@234bef66
System.out.println(instance1.getData() == instance1.getData()); // true

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);
EnumSingleton instance3 = (EnumSingleton) ois.readObject();
ois.close();

System.out.println(instance1.getData()); // java.lang.Object@234bef66
System.out.println(instance3.getData()); // java.lang.Object@234bef66
System.out.println(instance1.getData() == instance3.getData()); // true
} catch (Exception e) {
e.printStackTrace();
}

可以看出即便是序列化,也不能破坏枚举的单例性质。

为什么如此神奇呢?反编译 EnumSingleton.class 看看:

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
public final class EnumSingleton extends Enum {
public static EnumSingleton[] values() {
return (EnumSingleton[])$VALUES.clone();
}
public static EnumSingleton valueOf(String name) {
return (EnumSingleton)Enum.valueOf(com/guitu18/singleton/EnumSingleton, name);
}
private EnumSingleton(String s, int i) {
super(s, i);
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumSingleton getInstance() {
return INSTANCE;
}
public static final EnumSingleton INSTANCE;
private Object data;
private static final EnumSingleton $VALUES[];
static {
INSTANCE = new EnumSingleton("INSTANCE", 0);
$VALUES = (new EnumSingleton[] {
INSTANCE
});
}
}

代码最后,有一个静态代码块,是以饿汉式的方式对 INSTANCE 进行了赋值。那么我们并没有添加 readResolve 方法,序列化为什么没有破坏枚举式单例呢?还是点进去 readObject() 方法一探究竟,同上这里只贴关键部分:

1
2
3
4
5
6
7
8
switch (tc) {
...
case TC_ENUM:
return checkResolve(readEnum(unshared));
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
...
}

这里根据类型判断,进了 readEnum() 方法,继续点进去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private Enum<?> readEnum(boolean unshared) throws IOException {
...
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);
}
...
}
...
return result;
}

关键的一行代码:Enum<?> en = Enum.valueOf((Class)cl, name);点进去就到了

这里:
1
2
3
4
5
6
7
8
9
10
11

```java
public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) {
T result = enumType.enumConstantDirectory().get(name);
if (result != null)
return result;
if (name == null)
throw new NullPointerException("Name is null");
throw new IllegalArgumentException(
"No enum constant " + enumType.getCanonicalName() + "." + name);
}

看这一句 enumType.enumConstantDirectory().get(name) 猜测 enumConstantDirectory()返回的应该是一个 Map 类的东西,点进去看验证了猜想是对的,这里到达了 Class 类了。

1
2
3
4
5
6
7
8
9
10
11
12
13
Map<String, T> enumConstantDirectory() {
if (enumConstantDirectory == null) {
T[] universe = getEnumConstantsShared();
if (universe == null)
throw new IllegalArgumentException(
getName() + " is not an enum type");
Map<String, T> m = new HashMap<>(2 * universe.length);
for (T constant : universe)
m.put(((Enum<?>)constant).name(), constant);
enumConstantDirectory = m;
}
return enumConstantDirectory;
}

enumConstantDirectory() 方法返回了一个枚举名称和枚举常量的映射,在返回的这个 Map中我们能根据枚举名称获取到对应的枚举常量实例。

在上面的方法中,可以看到是先获取到枚举实例数组,然后遍历放入这个 Map 中的。这个数组是调用了 getEnumConstantsShared() 获取到的:

1
2
3
4
5
6
7
8
9
10
11
12
13
T[] getEnumConstantsShared() {
if (enumConstants == null) {
if (!isEnum()) return null;
try {
final Method values = getMethod("values");
...
T[] temporaryConstants = (T[])values.invoke(null);
enumConstants = temporaryConstants;
}
...
}
return enumConstants;
}

在这个方法中,先获取了一个名为 values 的方法,之后再调用该方法,将方法的返回值 return 回去。

这里的这个 values 方法其实就是我们反编译枚举类后看到的那个 values 方法:

1
2
3
4
5
6
7
8
9
10
11
public static EnumSingleton[] values() {
return (EnumSingleton[])$VALUES.clone();
}
public static final EnumSingleton INSTANCE;
private static final EnumSingleton $VALUES[];
static {
INSTANCE = new EnumSingleton("INSTANCE", 0);
$VALUES = (new EnumSingleton[] {
INSTANCE
});
}

values 方法返回的,正是保存着在 static 代码块中已经实例化好的枚举实例的 $VALUES 数组,至此谜团终于揭开。

那么回到前一步的readEnum() 方法,调用的 Enum.valueOf() 正是通过枚举的 Class 对象和 枚举名称 找到唯一的枚举实例。因此,序列化时枚举对象不会被类加载器加载多次。所以,枚举也是一种注册式单例

到这里已经证实了,序列化不会破坏枚举的单例特性。那么在 Java 中无所不能的反射能否攻破枚举式单例呢?

继续上代码测试:

1
2
3
4
5
6
7
8
9
10
try {
Class clazz = EnumSingleton.class;
// 获取私有构造方法
Constructor constructor = clazz.getDeclaredConstructor(null);
// 开启暴力访问,强吻,不愿意也要上
constructor.setAccessible(true);
EnumSingleton enumSingleton = (EnumSingleton) constructor.newInstance();
} catch (Exception e) {
e.printStackTrace();
}

执行直接报错了:

1
2
3
java.lang.NoSuchMethodException: com.guitu18.singleton.EnumSingleton.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)

找不到这样一个构造方法,通过前面的反编译我们也看到了,没有无参构造方法,只有一个两参数的构造方法:

private EnumSingleton(String s, int i) {
    super(s, i);
}

这里也是直接调用了父类的构造方法,那么我们就获取这个带参构造方法:

1
2
3
4
5
6
7
8
9
10
try {
Class clazz = EnumSingleton.class;
// 获取私有构造方法
Constructor constructor = clazz.getDeclaredConstructor(String.class, int.class);
// 开启暴力访问,强吻,不愿意也要上
constructor.setAccessible(true);
EnumSingleton enumSingleton = (EnumSingleton) constructor.newInstance("INSTANCE", 0);
} catch (Exception e) {
e.printStackTrace();
}

执行还是报错,不能通过反射创建枚举:

1
2
3
java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at com.guitu18.singleton.EnumSingletonTest.main(EnumSingletonTest.java:52)

通过打断点发现,我们能拿到这样一个构造方法,但是在调用 newInstance() 时报错,哪里有错点哪里,继续点进去 newInstance 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 执行时会检查类型,如果是枚举类型,在实例化时直接抛出异常。

至此,也证实了“枚举现如今已成为实现Singleton的最佳方法”这句话,在《Effective
Java》一书中也推荐使用枚举实现单例。

这一切的一切都是源于 JDK 枚举的语法特殊性,还有那个在 Java无所不能的反射也在为枚举保驾护航,使得枚举式单例成为单例模式的一种最优雅的实现


# ThreadLocal 线程单例

ThreadLocal 线程单例其实应该也算是注册式单例的一种,它是利用 ThreadLocal 的特点,保证在同一个线程内一个对象是单例的。

精简版代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ThreadLocalSingleton {
private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance =
new ThreadLocal<ThreadLocalSingleton>() {
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}
};

private ThreadLocalSingleton() {
}

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

代码比较简单,直接上测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) {
String tName = Thread.currentThread().getName();
System.out.println(tName + " : " + ThreadLocalSingleton.getInstance());
System.out.println(tName + " : " + ThreadLocalSingleton.getInstance());
System.out.println(tName + " : " + ThreadLocalSingleton.getInstance());
System.out.println(tName + " : " + ThreadLocalSingleton.getInstance());
System.out.println(tName + " : " + ThreadLocalSingleton.getInstance());

for (int i = 0; i < 5; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
String tName = Thread.currentThread().getName();
System.out.println(tName + ":" + ThreadLocalSingleton.getInstance());
}
});
thread.start();
}
}

控制台输出:

1
2
3
4
5
6
7
8
9
10
main : com.guitu18.singleton.ThreadLocalSingleton@4554617c
main : com.guitu18.singleton.ThreadLocalSingleton@4554617c
main : com.guitu18.singleton.ThreadLocalSingleton@4554617c
main : com.guitu18.singleton.ThreadLocalSingleton@4554617c
main : com.guitu18.singleton.ThreadLocalSingleton@4554617c
Thread-0:com.guitu18.singleton.ThreadLocalSingleton@50e1d3f3
Thread-2:com.guitu18.singleton.ThreadLocalSingleton@6a456558
Thread-1:com.guitu18.singleton.ThreadLocalSingleton@4e8a6c13
Thread-4:com.guitu18.singleton.ThreadLocalSingleton@13d8848c
Thread-3:com.guitu18.singleton.ThreadLocalSingleton@283323cb

可以看到,在同一个线程中,获取的都是用一个对象,在不通的线程中,获取的是不同的对象。

这种模式在一些特定的场景有着特殊的作用,比如数据库连接池,多数据源切换等。它保证了在同一个线程内所获得的都是同一个连接。

单例模式分析完毕,如有不完善的地方还望大家指出,互勉。Java 之路很长,还要继续努力。

明人不说暗话,如果你觉得可以的话,你懂的!