上篇文章介绍了基本的单例模式,也是大家听说比较多的单例模式写法,这几种单例模式每个都有自己的问题,
比如看似完美的静态内部类单例写法也会被反射所破坏;当然我们是有解决方案的,比如
private SingleInnerClassInstance() {
if (InnerSingleInstance.instance != null) {
throw new RuntimeException("非法创建");
}
}
我在构造方法加上这句代码就可解决这个问题,但是这样显然是不够优雅的,说白了就是不够官方,下面我来看下官方的写法。
注册式单例
注册式单例其实是利用Java的枚举类来实现的,每个枚举类型都是通过valueOf方法来获取到枚举本身,其底层数据结构是一个map,key是当前枚举值的name,value是枚举类
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);
}
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;
}
所以我们简单看了枚举类源码之后可以利用这个特点写一个单例代码如下:
public enum RegisteredSingleInstance {
INSTANCE;
private Object data;
public static RegisteredSingleInstance getInstance(){
return INSTANCE;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
public class Test {
public static void main(String[] args) {
RegisteredSingleInstance instance = RegisteredSingleInstance.getInstance();
//使用set注入的方式赋值
instance.setData(new Object());
Object data = instance.getData();
System.out.println(data);
}
}
那么注册式单例是否能被反射破坏呢? 我们测试一下
Class<RegisteredSingleInstance> clazz = RegisteredSingleInstance.class;
Constructor<RegisteredSingleInstance> constructor = clazz.getDeclaredConstructor(String.class,int.class);
constructor.setAccessible(true);
RegisteredSingleInstance instance1 = constructor.newInstance(null);
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at com.designpatterns.lishi.single.registered.Test.main(Test.java:26)
//源码
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
结果发现在构造方法处加了判断,如果是枚举类是不能调用构造方法的。
枚举看似完美,但是通过javap反编译之后我们发现枚举类定义的枚举值全部生明在static{}代码块中,也就是说类加载的时候就会初始化这些对象,那这就又回到我们最初的饿汉式存在的问题,当有大量单例式我们都用枚举式来实现会产生大量内存浪费。
static {};
Code:
0: new #4 // class com/designpatterns/lishi/single/registered/RegisteredSingleInstance
3: dup
4: ldc #9 // String INSTANCE
6: iconst_0
7: invokespecial #10 // Method "<init>":(Ljava/lang/String;I)V
10: putstatic #7 // Field INSTANCE:Lcom/designpatterns/lishi/single/registered/RegisteredSingleInstance;
13: iconst_1
14: anewarray #4 // class com/designpatterns/lishi/single/registered/RegisteredSingleInstance
17: dup
18: iconst_0
19: getstatic #7 // Field INSTANCE:Lcom/designpatterns/lishi/single/registered/RegisteredSingleInstance;
22: aastore
23: putstatic #1 // Field $VALUES:[Lcom/designpatterns/lishi/single/registered/RegisteredSingleInstance;
容器式单例
为了解决这个问题,容器式单例应运而生,它是在借鉴了枚举式单例的写法,统一将所有单例对象存到一个map中代为如下
public class ContainerSingleInstance {
private ContainerSingleInstance() {
}
private static Map<String, Object> container = new ConcurrentHashMap<>();
public static Object getInstance(String name) {
if (!container.containsKey(name)) {
Object o;
try {
Class<?> aClass = Class.forName(name);
o = aClass.newInstance();
container.put(name, o);
return o;
} catch (Exception e) {
e.printStackTrace();
}
} else {
return container.get(name);
}
return null;
}
}
这样就避免了内存浪费,上述代码有线程安全问题,可以借鉴双层检查锁来解决。
ThreadLocal式单例
接下来是最特殊的一种单例写法,为什么特殊呢是因为这种写法会保证每个线程拿到同一个对象,不同线程互相不影响。
public class ThreadLocalSingleInstance {
private static final ThreadLocal<ThreadLocalSingleInstance> threadLocal = ThreadLocal.withInitial(ThreadLocalSingleInstance::new);
private ThreadLocalSingleInstance() {
}
public static ThreadLocalSingleInstance getInstance() {
return threadLocal.get();
}
}
//测试
public static void main(String[] args) {
//lambda
new Thread(() -> {
ThreadLocalSingleInstance instance = ThreadLocalSingleInstance.getInstance();
ThreadLocalSingleInstance instance2 = ThreadLocalSingleInstance.getInstance();
System.out.println(Thread.currentThread().getName() + instance);
System.out.println(Thread.currentThread().getName() + instance2);
}).start();
new Thread(() -> {
ThreadLocalSingleInstance instance = ThreadLocalSingleInstance.getInstance();
ThreadLocalSingleInstance instance2 = ThreadLocalSingleInstance.getInstance();
System.out.println(Thread.currentThread().getName() + instance);
System.out.println(Thread.currentThread().getName() + instance2);
}).start();
}
//输出
Thread-0com.designpatterns.lishi.single.threadlocal.ThreadLocalSingleInstance@7e75a754
Thread-0com.designpatterns.lishi.single.threadlocal.ThreadLocalSingleInstance@7e75a754
Thread-1com.designpatterns.lishi.single.threadlocal.ThreadLocalSingleInstance@78541db1
Thread-1com.designpatterns.lishi.single.threadlocal.ThreadLocalSingleInstance@78541db1
结果是符合上述说明的哈~
总结一下单例模式简单可分为5大类:饿汉式、懒汉式、注册式、容器式、ThreadLocal式。
若单例对象不多的情况下推荐使用注册式-枚举来实现,这个是目前最优雅且最贴合单例的一种写法,我们在使用某一个技术一定要清楚为什么会有该技术,怎么用该技术,该技术有什么问题以及如何解决。这样才是正确的思考方式
学习是一条漫无止境的路,加油!持续更新中……