# 源码学习系列:设计模式之单例模式
单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。单例模式是创建型模式。单例模式在现实生活中应用也非常广泛。 例如,国家主席、公司 CEO、部门经理等。在 J2EE 标准中,ServletContext、ServletContextConfig 等;在 Spring 框架应用中 ApplicationContext;数据库的连接池也都是单例形式。
# 本篇目录
- 饿汉式单例
- 懒汉式单例
- 反射破坏单例
- 序列化破坏单例
- 注册式单例之容器缓存
- 注册式单例之枚举单例
- ThreadLocal 线程单例
# 饿汉式单例
先看代码:
public class HungrySingleton {
private static final HungrySingleton HUNGRY_SINGLETON = new HungrySingleton();
private HungrySingleton() {}
public static HungrySingleton getInstance() {
return HUNGRY_SINGLETON;
}
}
代码非常简单,使用了static关键字,在类被加载的时候就进行实例化,并私有构造方法,提供一个静态方法返回实例的引用。这里的静态成员变量也可以使用静态代码块进行初始化,如:
static {
HUNGRY_SINGLETON = new HungryStaticSingleton();
}
这种方式简单有效,没有任何形式的锁,所以不存在效率问题。但缺点是不管类有没有被使用,只要被加载就回初始化,会造成资源浪费,当这样的单例非常多的是时候,是需要占用很大的内存空间的。
# 懒汉式单例
为了解决上面的的问题,懒汉式单例应时而生:
public class LazySimpleSingleton {
private static LazySimpleSingleton lazy = null;
private LazySimpleSingleton() {}
public synchronized static LazySimpleSingleton getInstance() {
if (lazy == null) {
lazy = new LazySimpleSingleton();
}
return lazy;
}
}
这里是在需要获取实例的时候才会进行初始化,方法内部做了一个 null
判断,保证只会被初始化一次,并且为了保证线程安全加了同步锁。
这样的好处是,只有在类真正需要调用的时候才会被初始化,但是因为加了同步锁,会导致在多线程环境下产生性能问题。需要继续优化,看代码:
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
代码中,虽然只有一次,但是终究是不完美的。
再看看下面的代码:
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()
方法,我用反射调用呢?
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
大反射就是这么任性,就喜欢看你看不惯我又干不掉我的样子,哈哈。
你说,那我可以在私有构造方法中抛异常,比如:
private LazyInnerClassSingleton() {
if (LazyHolder.LAZY != null) {
throw new RuntimeException("不允许创建多个实例");
}
}
但这时又回到最开始的线程安全问题上了,多线程反射调用时,你要保证构造方法的线程安全,又要加锁,又要处理。等于又回到第一步第二部中的循环中去了。
# 序列化破坏单例
再看另一种情况,序列化。当我们将一个对象创建好后,有时候需要将对象写入磁盘或者通过网络流传输,需要先序列化,下次使用时,进行反序列化。但是反序列化之后的对象会重新分配内存,相当于重新创建。这种情况下也会破坏单例。以第一步饿汉式单例为例,看下面的代码:
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()
一探究竟:
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)
,继续点进去
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)
方法,在这个方法里我找到了答案:
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
方法就可以了,还是以饿汉式为例,看代码:
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()
:
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()
只做了一个判断:
boolean hasReadResolveMethod() {
requireInitialized();
return (readResolveMethod != null);
}
继续追踪这个成员变量 readResolveMethod
,我们在代码中找到了为其赋值的地方:
readResolveMethod = getInheritableMethod(cl, "readResolve", null, Object.class);
我们继续查看 getInheritableMethod()
这个方法做了什么:
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
中的实例。
大名鼎鼎的 Spring
的 IOC
容器 BeanFactory
就是注册式单例。在调用时,先判断 Map
中是否已有该实例,有就直接返回,没有就创建一个保存到 Map
中再返回。
下面是一个 IOC
容器的简化版实现,代码比较简单,就不做说明和测试了。
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);
}
}
}
}
# 注册式单例之枚举单例
再来看枚举式单例,枚举单例使用起来非常简单,代码如下:
public enum EnumSingleton {
INSTANCE;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
直接上测试代码:
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
看看:
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()
方法一探究竟,同上这里只贴关键部分:
switch (tc) {
...
case TC_ENUM:
return checkResolve(readEnum(unshared));
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
...
}
这里根据类型判断,进了 readEnum()
方法,继续点进去:
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);
点进去就到了 Enum
这里:
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
类了。
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()
获取到的:
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
方法:
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
中无所不能的反射能否攻破枚举式单例呢?
继续上代码测试:
try {
Class clazz = EnumSingleton.class;
// 获取私有构造方法
Constructor constructor = clazz.getDeclaredConstructor(null);
// 开启暴力访问,强吻,不愿意也要上
constructor.setAccessible(true);
EnumSingleton enumSingleton = (EnumSingleton) constructor.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
执行直接报错了:
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);
}
这里也是直接调用了父类的构造方法,那么我们就获取这个带参构造方法:
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();
}
执行还是报错,不能通过反射创建枚举:
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
方法:
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
的特点,保证在同一个线程内一个对象是单例的。
精简版代码实现:
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();
}
}
代码比较简单,直接上测试:
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();
}
}
控制台输出:
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
之路很长,还要继续努力。