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

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

# 本篇目录

  1. 饿汉式单例
  2. 懒汉式单例
  3. 反射破坏单例
  4. 序列化破坏单例
  5. 注册式单例之容器缓存
  6. 注册式单例之枚举单例
  7. 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 中的实例。

大名鼎鼎的 SpringIOC 容器 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 之路很长,还要继续努力。