注册式单例
注册式单例模式又称为登记式单例模式,就是将每一个实例都登记到某一个地方,使用唯一的标识获取实例。
注册式单例模式有两种:一种为枚举式单例模式,另一种为容器式单例模式。
枚举式单例模式
编写一个 Demo 示例说明
- 一个经典的枚举类单例
java
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;
}
}
- 编写测试类
java
public static void main(String[] args) {
EnumSingleton instance = EnumSingleton.getInstance();
instance.setData(new Object());
try {
Class clazz = EnumSingleton.class;
Constructor c = clazz.getDeclaredConstructor(String.class,int.class);
c.setAccessible(true);
// System.out.println(c);
Object o = c.newInstance();
// System.out.println(o);
}catch (Exception e){
e.printStackTrace();
}
}
- 运行看结果

返回异常提示,在源码中判断
(clazz.getModifiers() & Modifier.ENUM) != 0条件,当类的修饰类型为 Enum 时返回Cannot reflectively create enum objects,即无法通过反射创建枚举对象。我们还是习惯性地想来看看JDK源码,进入Constructor 的 newInstance()方法
java
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
Class<?> caller = override ? null : Reflection.getCallerClass();
return newInstanceWithCaller(initargs, !override, caller);
}
newInstanceWithCaller(Object[] args, boolean checkAccess, Class<?> caller)
throws InstantiationException, IllegalAccessException,
InvocationTargetException
{
if (checkAccess)
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(args);
return inst;
}
从上述代码可以看到,在 newInstance()方法中做了强制性的判断,如果修饰符是 Modifier.ENUM枚举类型,则直接抛出异常。这岂不是和静态内部类的处理方式有异曲同工之妙?对,但是我们自己再构造方法中写逻辑处理可能存在未知的风险,而JDK 的处理是最官方、最权威、最稳定的。因此枚举式单例模式也是《Effective Java》书中推荐的一种单例模式实现写法。
到此为止,我们是不是已经非常清晰明了呢?JDK 枚举的语法特殊性及反射也为枚举保驾护航,让枚举式单例模式成为一种比较优雅的实现。
至此我们知道了枚举式单例可以防止反射破坏单例,那线程安全又从何谈起呢?我们不妨从 Enum 类型源码中 valueOf 方法中看看
java
public static <T extends Enum<T>> T valueOf(Class<T> enumClass,
String name) {
T result = enumClass.enumConstantDirectory().get(name);
if (result != null)
return result;
if (name == null)
throw new NullPointerException("Name is null");
throw new IllegalArgumentException(
"No enum constant " + enumClass.getCanonicalName() + "." + name);
}
java
Map<String, T> enumConstantDirectory() {
Map<String, T> directory = enumConstantDirectory;
if (directory == null) {
T[] universe = getEnumConstantsShared();
if (universe == null)
throw new IllegalArgumentException(
getName() + " is not an enum class");
directory = new HashMap<>((int)(universe.length / 0.75f) + 1);
for (T constant : universe) {
directory.put(((Enum<?>)constant).name(), constant);
}
enumConstantDirectory = directory;
}
return directory;
}
java
private transient volatile Map<String, T> enumConstantDirectory;
Enum 类型他的每个单例在类类声明的时候就创建好了,通过 valueOf 方法进行获取,而我们通过 valueOf 源码可以得知,当声明枚举时,该枚举就被当成常量
enumConstantDirectory注册到 Map 容器中,其创建方式类似于饿汉式,当大批量创建对象时,又会造成内存浪费
容器式单例
其实枚举式单例,虽然写法优雅,但是也会有一些问题。因为它在类加载之时就将所有的对象初始化放在类内存中,这其实和饿汉式并无差异,不适合大量创建单例对象的场景。那么,接下来看注册式单例模式的另一种写法,即容器式单例模式,创建 ContainerSingleton 类:
java
public class ContainerSingleton {
//构造方法私有化
private ContainerSingleton(){}
//创建一个ioc容器
private static Map<String,Object> ioc = new ConcurrentHashMap<String, Object>();
public static Object getInstance(String className){
Object instance = null;
if(!ioc.containsKey(className)){
try {
instance = Class.forName(className).newInstance();
ioc.put(className, instance);
}catch (Exception e){
e.printStackTrace();
}
return instance;
}else{
return ioc.get(className);
}
}
}
- 创建一个 Pojo 类,通过 ioc 容器创建
java
public class Pojo {}
- 编写测试类
java
public class ContainerSingletonTest {
public static void main(String[] args) {
Object instance1 = ContainerSingleton.getInstance("com.gupaoedu.vip.pattern.singleton.test.Pojo");
Object instance2 = ContainerSingleton.getInstance("com.gupaoedu.vip.pattern.singleton.test.Pojo");
System.out.println(instance1 == instance2);
}
}
- 运行结果

容器式单例如何解决线程安全问题?
ThreadLocal 单例
保证线程内部全局唯一,且天生线程安全
- 编写一个简单的 Demo
java
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();
}
}
java
public static void main(String[] args) {
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(ThreadLocalSingleton.getInstance());
}
- 查看结果,在线程无关时,满足单实例

模拟多线程情况下,单例是否被破坏
- 修改代码
ExecutorThread,
java
public class ExectorThread implements Runnable{
public void run() {
// LazySimpleSingleton instance = LazySimpleSingleton.getInstance();
ThreadLocalSingleton instance = ThreadLocalSingleton.getInstance();
System.out.println(Thread.currentThread().getName() + ":" + instance);
}
}
- 查看结果

我们发现,在主线程中无论调用多少次,获取到的实例都是同一个,都在两个子线程中分别获取到了不同的实例。那么 ThreadLocal 是如何实现这样的效果的呢?我们知道,单例模式为了达到线程安全的目的,会给方法上锁,以时间换空间。ThreadLoca 将所有的对象全部放在 ThreadLocalMap 中为每个线程都提供一个对象,实际上是以空间换时间来实现线程隔离的。
- 可以看到单例在多线程情况下,单实例被"破坏"了,但实际上是正常的没因为
ThreadLocal 单例是维持在线程内部单例,不同线程下可多实例ThreadLocal本身是基于线程的,main本身就是就是一个线程,因此前三次输出的为mian线程中的单例,都是一致的,而后面Thread-0和Thread-1都是单独的另外的线程
我们需要思考一下,ThreadLocal是如何实现线程之间的单例隔离呢?这里我们需要进入
ThreadLocal类中的一个方法get
java
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
进入方法之后先判断 map 对象是否为空,若为空,以当前调用 get 方法的线程作为单例实例对象存入到 map 容器中,作为 key 值,并将线程信息存入到 map 中
单例模式在源码中的应用
- 在 SpringBoot 源码中的应用
AbstractFactoryBean.class
java
..........
public final T getObject() throws Exception {
if (this.isSingleton()) {
return (T)(this.initialized ? this.singletonInstance : this.getEarlySingletonInstance());
} else {
return (T)this.createInstance();
}
}
private T getEarlySingletonInstance() throws Exception {
Class[] ifcs = this.getEarlySingletonInterfaces();
if (ifcs == null) {
throw new FactoryBeanNotInitializedException(this.getClass().getName() + " does not support circular references");
} else {
if (this.earlySingletonInstance == null) {
this.earlySingletonInstance = (T)Proxy.newProxyInstance(this.beanClassLoader, ifcs, new EarlySingletonInvocationHandler());
}
return this.earlySingletonInstance;
}
}
..........
- MyBatis (3.5)中的
ErrorContext.class - 来看一个JDK 的一个经典应用 Runtime 类,其源码如下
java
public class Runtime {
private static final Runtime currentRuntime = new Runtime();
private static Version version;
public static Runtime getRuntime() {
return currentRuntime;
}
private Runtime() {}
........
}
单例模式的优缺点
- 优点
- 在内存中只有一个实例,减少内存开销
- 可以避免对资源的多重占用
- 设置全局访问点,严格控制访问
- 缺点
- 没有接口,扩展困难
- 若需扩展单例对象,只能修改代码,耦合度高
使用单例时应考关注的方向
- 私有化构造方法
- 是否多线程,是否保证线程安全
- 是否可延时加载
- 是否能够防止序列化与反序列化破坏单例
- 是否防御反射攻击单例
