本专栏内容参考自:咕泡学院Tom老师的《Spring5核心原理与30个类手写实战》,仅作个人学习记录使用,如有侵权,联系速删。
单例模式的定义
单例模式(Singleton Pattern )是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。单例模式是创建型模式。单例模式在现实生活中应用也非常广泛,例如,公司CEO、部门经理等。J2EE标准中的ServletContext,ServletContextConfig等、Spring框架应用中的ApplicationContext、数据库的连接池等也都是单例形式。
饿汉式单例模式
饿汉式单例模式在类加载的时候就立即初始化,并且创建单例对象。它绝对线程安全,在线程还没 出现以前就实例化了,不可能存在访问安全问题。
代码语言:javascript复制/**
* 优点:执行效率高,性能高,没有任何的锁
* 缺点:某些情况下,可能会造成内存浪费
*/
public class HungrySingleton {
private static final HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton(){}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
还有另外一种写法,利用静态代码块的机制:
代码语言:javascript复制public class HungryStaticSingleton {
//先静态后动态
//先上,后下
//先属性后方法
private static final HungryStaticSingleton hungrySingleton;
//装个B
static {
hungrySingleton = new HungryStaticSingleton();
}
private HungryStaticSingleton(){}
public static HungryStaticSingleton getInstance(){
return hungrySingleton;
}
}
这两种写法都非常简单,也非常好理解,饿汉式单例模式适用于单例对象较少的情况。这样写可以保证绝对线程安全、执行效率比较高。但是它的缺点也很明显,就是所有对象类加载的时候就实例化。
这样一来,如果系统中有大批量的单例对象存在,那系统初始化是就会导致大量的内存浪费。也就是说,
不管对象用与不用都占着空间,浪费了内存,有可能“占着茅坑不拉屎”。那有没有更优的写法呢?下
面我们来继续分析。
懒汉式单例模式
为了解决饿汉式单例可能带来的内存浪费问题,于是就出现了懒汉式单例的写法,懒汉式单例模式的特点是,单例对象要在被使用时才会初始化,下面看懒汉式单例模式的简单实现 LazySimpleSingleton :
代码语言:javascript复制/**
* 优点:节省了内存,线程安全
* 缺点:性能低
*/
public class LazySimpleSingletion {
private static LazySimpleSingletion instance;
private LazySimpleSingletion(){}
public static LazySimpleSingletion getInstance(){
if(instance == null){
instance = new LazySimpleSingletion();
}
return instance;
}
}
但这样写又带来了一个新的问题,如果在多线程环境下,就会出现线程安全问题。我先来模拟一下, 编写线程类ExectorThread :
代码语言:javascript复制public class ExectorThread implements Runnable{
@Override
public void run() {
LazySimpleSingletion instance = LazySimpleSingletion.getInstance();
LazyDoubleCheckSingleton instance = LazyDoubleCheckSingleton.getInstance();
}
}
客户端测试代码如下:
代码语言:javascript复制public class LazySimpleSingletonTest {
public static void main(String[] args) {
Thread t1 = new Thread(new ExectorThread());
Thread t2 = new Thread(new ExectorThread());
t1.start();
t2.start();
System.out.println("End");
}
}
运行结果:
果然,上面的代码有一定概率出现两种不同结果,这意味着上面的单例存在线程安全隐患
。
我们打上断点,一步一步调试,通过不断切换线程,并观测其内存状态,我们发现在线程环境下LazySimpleSingleton被实例化了两次。有时我们得到的运行结果可能是相同的两个对象,实际上是被后面执行的线程覆盖了,我们看到了一个假象,线程安全隐患依旧存在。那么,我们如何来优化代码,使得懒汉式单例模式在线程环境下安全呢?来看下面的代码给getInstance()加上synchronized关键字,使这个方法变成线程同步方法∶
public class LazySimpleSingletion {
private static LazySimpleSingletion instance;
private LazySimpleSingletion(){}
public synchronized static LazySimpleSingletion getInstance(){
if(instance == null){
instance = new LazySimpleSingletion();
}
return instance;
}
}
我们再来调试。当执行其中一个线程并调用getInstance()方法时,另一个线程在调用getInstance()方法,线程的状态由RUNNING变成了MONITOR,出现阻塞。直到第一个线程执行完,第二个线程才恢复到RUNNING状态继续调用getInstance(方法,如下图所示。
上图完美地展现了synchronized监视锁的运行状态,线程安全的问题解决了。但是,用synchronized加锁时,在线程数量比较多的情况下,如果CPU分配压力上升,则会导致大批线程阻塞,从而导致程序性能大幅下降。那么,有没有一种更好的方式,既能兼顾线程安全又能提升程序性能呢?答案是肯定的。我们来看双重检查锁的单例模式∶
代码语言:javascript复制/**
* 优点:性能高了,线程安全了
* 缺点:可读性难度加大,不够优雅
*/
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton instance;
private LazyDoubleCheckSingleton(){}
public static LazyDoubleCheckSingleton getInstance(){
//检查是否要阻塞
if (instance == null) {
synchronized (LazyDoubleCheckSingleton.class) {
//检查是否要重新创建实例
if (instance == null) {
instance = new LazyDoubleCheckSingleton();
//指令重排序的问题
}
}
}
return instance;
}
}
当第一个线程调用getInstance()方法时,第二个线程也可以调用。当第一个线程执行到synchronized时会上锁,第二个线程就会变成MONITOR状态,出现阻塞。此时,阻塞并不是基于整个LazySimpleSingleton类的阻塞,而是在getInstance()方法内部的阻塞,只要逻辑不太复杂,对于调用者而言感知不到。 但是,用到synchronized 关键字总归要上锁,对程序性能还是存在一定影响的。难道就真的没有 更好的方案吗﹖当然有。我们可以从类初始化的角度来考虑,看下面的代码,采用静态内部类的方式:
代码语言:javascript复制public class LazyStaticInnerClassSingleton {
private LazyStaticInnerClassSingleton(){
}
private static final LazyStaticInnerClassSingleton getInstance(){
return LazyHolder.INSTANCE;
}
private static class LazyHolder{
private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
}
}
这种方式兼顾了饿汉式单例模式的内存浪费问题和synchronized的性能问题。内部类一定是要在方法调用之前初始化,巧妙地避免了线程安全问题。由于这种方式比较简单,我就不带大家一步一步调试了。但是,金无足赤,人无完人,单例模式亦如此。这种写法真的就完美了吗?
反射破坏单例
现在我们来看一个事故现场。大家有没有发现,上面介绍的单例模式的构造方法除了加上private 关键字,没有做任何处理。如果我们使用反射来调用其构造方法,再调用getInstance()方法,应该有 两个不同的实例。现在来看一段测试代码,以LazyInnerClassSingleton为例:
代码语言:javascript复制public class ReflectTest {
public static void main(String[] args) {
try {
//在很无聊的情况下进行破坏
Class<?> clazz = LazyStaticInnerClassSingleton.class;
//通过反射获取私有的构造方法
Constructor c = clazz.getDeclaredConstructor(null);
//授权强制访问
c.setAccessible(true);
//实例化
Object instance1 = c.newInstance();
//再次实例化
Object instance2 = c.newInstance();
System.out.println(instance1);
System.out.println(instance2);
System.out.println(instance1 == instance2);
}catch (Exception e){
e.printStackTrace();
}
}
}
运行结果如下图:
显然,创建了两个不同的实例,不符合单例模式的定义
。那怎么办呢?我们来做一次优化。现在,我们在其构造方法中做一
些限制,一旦出现多次重复创建,则直接抛出异常。来看优化后的代码︰
public class LazyStaticInnerClassSingleton {
private LazyStaticInnerClassSingleton(){
if(LazyHolder.INSTANCE != null){
throw new RuntimeException("不允许非法访问");
}
}
private static LazyStaticInnerClassSingleton getInstance(){
return LazyHolder.INSTANCE;
}
private static class LazyHolder{
private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
}
}
再次运行,会得到以下结果:
至此,自认为史上最牛的单例模式的实现方式便大功告成,避免了线程问题,反射破坏,性能尚可,且写法优雅。但是,上面看似完美的单例写法还是有可能被破坏。
序列化破坏单例
一个单例对象创建好后,有时候需要将对象序列化然后写入磁盘,下次使用时再从磁盘中读取对象并进行反序列化,将其转化为内存对象。反序列化后的对象会重新分配内存,即重新创建。如果序列化的目标对象为单例对象,就违背了单例模式的初衷,相当于破坏了单例,来看一段代码︰
代码语言:javascript复制public class SeriableSingleton implements Serializable {
//序列化
//把内存中对象的状态转换为字节码的形式
//把字节码通过IO输出流,写到磁盘上
//永久保存下来,持久化
//反序列化
//将持久化的字节码内容,通过IO输入流读到内存中来
//转化成一个Java对象
public final static SeriableSingleton INSTANCE = new SeriableSingleton();
private SeriableSingleton(){}
public static SeriableSingleton getInstance(){
return INSTANCE;
}
}
测试代码:
代码语言:javascript复制public class SeriableSingletonTest {
public static void main(String[] args) {
SeriableSingleton s1 = null;
SeriableSingleton s2 = SeriableSingleton.getInstance();
FileOutputStream fos = null;
try {
fos = new FileOutputStream("SeriableSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s2);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (SeriableSingleton)ois.readObject();
ois.close();
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行结果如下图:
从运行结果可以看出,反序列化后的对象和手动创建的对象是不一致的,实例化了两次,违背了单例模式的设计初衷。那么,我们如何保证在序列化的情况下也能够实现单例模式呢﹖其实很简单,只需要增加readResolve()方法即可。来看优化后的代码︰
代码语言:javascript复制public class SeriableSingleton implements Serializable {
//序列化
//把内存中对象的状态转换为字节码的形式
//把字节码通过IO输出流,写到磁盘上
//永久保存下来,持久化
//反序列化
//将持久化的字节码内容,通过IO输入流读到内存中来
//转化成一个Java对象
public final static SeriableSingleton INSTANCE = new SeriableSingleton();
private SeriableSingleton(){}
public static SeriableSingleton getInstance(){
return INSTANCE;
}
//添加此方法即可
private Object readResolve(){
return INSTANCE;
}
}
再次运行,结果如下:
大家一定会想∶这是什么原因呢﹖为什么要这样写?看上去很神奇的样子,也让人有些费解。不如我们一起来看看JDK的源码实现以了解清楚。我们进入ObjectInputStream类的readObject()方法, 代码如下:
代码语言:javascript复制private final Object readObject(Class<?> type)
throws IOException, ClassNotFoundException
{
if (enableOverride) {
return readObjectOverride();
}
if (! (type == Object.class || type == String.class))
throw new AssertionError("internal error");
// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
Object obj = readObject0(type, false);
handles.markDependency(outerHandle, passHandle);
ClassNotFoundException ex = handles.lookupException(passHandle);
if (ex != null) {
throw ex;
}
if (depth == 0) {
vlist.doCallbacks();
}
return obj;
} finally {
passHandle = outerHandle;
if (closed && depth == 0) {
clear();
}
}
}
我们可以发现,代码中调用了重写的readObject()方法,让我们点进去:
代码语言:javascript复制private Object readObject0(Class<?> type, boolean unshared) throws IOException {
boolean oldMode = bin.getBlockDataMode();
if (oldMode) {
int remain = bin.currentBlockRemaining();
if (remain > 0) {
throw new OptionalDataException(remain);
} else if (defaultDataEnd) {
/*
* Fix for 4360508: stream is currently at the end of a field
* value block written via default serialization; since there
* is no terminating TC_ENDBLOCKDATA tag, simulate
* end-of-custom-data behavior explicitly.
*/
throw new OptionalDataException(true);
}
bin.setBlockDataMode(false);
}
byte tc;
while ((tc = bin.peekByte()) == TC_RESET) {
bin.readByte();
handleReset();
}
depth ;
totalObjectRefs ;
try {
switch (tc) {
case TC_NULL:
return readNull();
case TC_REFERENCE:
// check the type of the existing object
return type.cast(readHandle(unshared));
case TC_CLASS:
if (type == String.class) {
throw new ClassCastException("Cannot cast a class to java.lang.String");
}
return readClass(unshared);
case TC_CLASSDESC:
case TC_PROXYCLASSDESC:
if (type == String.class) {
throw new ClassCastException("Cannot cast a class to java.lang.String");
}
return readClassDesc(unshared);
case TC_STRING:
case TC_LONGSTRING:
return checkResolve(readString(unshared));
case TC_ARRAY:
if (type == String.class) {
throw new ClassCastException("Cannot cast an array to java.lang.String");
}
return checkResolve(readArray(unshared));
case TC_ENUM:
if (type == String.class) {
throw new ClassCastException("Cannot cast an enum to java.lang.String");
}
return checkResolve(readEnum(unshared));
case TC_OBJECT:
if (type == String.class) {
throw new ClassCastException("Cannot cast an object to java.lang.String");
}
return checkResolve(readOrdinaryObject(unshared));
case TC_EXCEPTION:
if (type == String.class) {
throw new ClassCastException("Cannot cast an exception to java.lang.String");
}
IOException ex = readFatalException();
throw new WriteAbortedException("writing aborted", ex);
case TC_BLOCKDATA:
case TC_BLOCKDATALONG:
if (oldMode) {
bin.setBlockDataMode(true);
bin.peek(); // force header read
throw new OptionalDataException(
bin.currentBlockRemaining());
} else {
throw new StreamCorruptedException(
"unexpected block data");
}
case TC_ENDBLOCKDATA:
if (oldMode) {
throw new OptionalDataException(true);
} else {
throw new StreamCorruptedException(
"unexpected end of block data");
}
default:
throw new StreamCorruptedException(
String.format("invalid type code: X", tc));
}
} finally {
depth--;
bin.setBlockDataMode(oldMode);
}
}
代码挺长,我们注意一下这一段
代码语言:javascript复制case TC_OBJECT:
if (type == String.class) {
throw new ClassCastException("Cannot cast an object to java.lang.String");
}
return checkResolve(readOrdinaryObject(unshared));
我们看到里面调用了readOrdinaryObject()方法,让我们再点进去看看:
代码语言:javascript复制private Object readOrdinaryObject(boolean unshared)
throws IOException
{
if (bin.readByte() != TC_OBJECT) {
throw new InternalError();
}
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) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
passHandle = handles.assign(unshared ? unsharedMarker : obj);
ClassNotFoundException resolveEx = desc.getResolveException();
if (resolveEx != null) {
handles.markException(passHandle, resolveEx);
}
if (desc.isExternalizable()) {
readExternalData((Externalizable) obj, desc);
} else {
readSerialData(obj, desc);
}
handles.finish(passHandle);
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;
}
注意一下里面的isInstantiable(),在这一段
代码语言:javascript复制Object obj;
try {
//这里这里
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
点进去,可以看到里面很简单:
代码语言:javascript复制 boolean isInstantiable() {
requireInitialized();
return (cons != null);
}
判断了一下构造方法是否为空,不为空就返回true,这意味着只要有无参的构造方法,就会实例化。
然后再往上返回,回到ObjectInputStream的readOrdinaryObject()方法,找到这一块
代码语言:javascript复制 if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
我们可以看到代码里在判断无参构造方法之后,又调用了hasReadResolveMethod()这个方法,来看代码:
代码语言:javascript复制 boolean hasReadResolveMethod() {
requireInitialized();
return (readResolveMethod != null);
}
就两三行,逻辑很简单,判断readResolveMethod 是否为空,不为空就返回true,那么readResolveMethod是在哪里赋值的呢?通过全局查找知道,在私有方法ObjectStreamClass()中给readResolveMethod进行了赋值,来看代码︰
代码语言:javascript复制//533行
readResolveMethod = getInheritableMethod(
cl, "readResolve", null, Object.class);
上面的逻辑其实就是通过反射找到一个无参的 readResolve()方法,并且保存下来。现在回到ObjectinputStream的readOrdinaryObject(方法继续往下看,如果readResolve()方法存在则调用invokeReadResolve()方法,来看代码︰
代码语言:javascript复制Object invokeReadResolve(Object obj)
throws IOException, UnsupportedOperationException
{
requireInitialized();
if (readResolveMethod != null) {
try {
return readResolveMethod.invoke(obj, (Object[]) null);
} catch (InvocationTargetException ex) {
Throwable th = ex.getTargetException();
if (th instanceof ObjectStreamException) {
throw (ObjectStreamException) th;
} else {
throwMiscException(th);
throw new InternalError(th); // never reached
}
} catch (IllegalAccessException ex) {
// should not occur, as access checks have been suppressed
throw new InternalError(ex);
}
} else {
throw new UnsupportedOperationException();
}
}
我们可以看到,在invokeReadResolve()方法中用反射调用了readResolveMethod方法。 通过JDK源码分析我们可以看出,虽然增加readResolve()方法返回实例解决了单例模式被破坏的 问题,但是实际上实例化了两次,只不过新创建的对象没有被返回而已。如果创建对象的动作发生频率 加快,就意味着内存分配开销也会随之增大,难道真的就没办法从根本上解决问题吗﹖下面讲的注册式单例也许能帮助到你。
注册式单例模式
注册式单例模式又称为登记式单例模式,就是将每一个实例都登记到某一个地方,使用唯一的标识 获取实例。注册式单例模式有两种:一种为枚举式单例模式,另一种为容器式单例模式。
1 枚举式单例模式
先来看枚举式单例模式的写法,来看代码,创建EnumSingleton类:
代码语言:javascript复制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;
}
}
写一下测试代码:
代码语言:javascript复制public class EnumSingletonTest2 {
public static void main(String[] args) {
try {
EnumSingleton instance1 = null;
EnumSingleton instance2 = EnumSingleton.getInstance();
instance2.setData(new Object());
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);
instance1 = (EnumSingleton) ois.readObject();
ois.close();
System.out.println(instance1.getData());
System.out.println(instance2.getData());
System.out.println(instance1.getData() == instance2.getData());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
结果如下:
没有做任何处理,我们发现运行结果和预期的一样。那么枚举式单例模式如此神奇,它的神秘之处 在哪里体现呢?下面通过分析源码来揭开它的神秘面纱。 下载一个非常好用的Java 反编译工具Jad(下载地址: https://varaneckas.com/jad/ ),解压后配置好环境变量(这里不做详细介绍),就可以使用命令行调用了。找到工程所在的Class目录,复制EnumSingleton.class所在的路径,如下图所示。
然后切换到命令行,切换到工程所在的Class目录,输入命令jad并在后面输入复制好的路径,在 Class目录下会多出一个EnumSingleton.jad文件。打开EnumSingleton.jad文件我们惊奇地发现有 如下代码︰
代码语言:javascript复制static
{
INSTANCE = new EnumSingLeton("INSTANCE",0);
$VALUES = (new EnumSingleton[] {
INSTANCE
});
}
原来,枚举式单例模式在静态代码块中就给INSTANCE进行了赋值,是饿汉式单例模式的实现。至此,我们还可以试想,序列化能否破坏枚举式单例模式呢?不妨再来看一下JDK源码,还是回到ObjectInputStream的readObject0(方法∶找到这一块
代码语言:javascript复制 case TC_ENUM:
if (type == String.class) {
throw new ClassCastException("Cannot cast an enum to java.lang.String");
}
return checkResolve(readEnum(unshared));
我们看到,在readObject0(中调用了readEnum()方法,来看readEnum()方法的代码实现︰
代码语言:javascript复制 private Enum<?> readEnum(boolean unshared) throws IOException {
if (bin.readByte() != TC_ENUM) {
throw new InternalError();
}
ObjectStreamClass desc = readClassDesc(false);
if (!desc.isEnum()) {
throw new InvalidClassException("non-enum class: " desc);
}
int enumHandle = handles.assign(unshared ? unsharedMarker : null);
ClassNotFoundException resolveEx = desc.getResolveException();
if (resolveEx != null) {
handles.markException(enumHandle, resolveEx);
}
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);
}
if (!unshared) {
handles.setObject(enumHandle, result);
}
}
handles.finish(enumHandle);
passHandle = enumHandle;
return result;
}
我们发现,枚举类型其实通过类名和类对象类找到一个唯一的枚举对象。因此,枚举对象不可能被 类加载器加载多次。那么反射是否能破坏枚举式单例模式呢﹖来看一段测试代码︰
代码语言:javascript复制public class EnumSingletonTest3 {
public static void main(String[] args) {
try {
Class clazz = EnumSingleton.class;
Constructor c = clazz.getDeclaredConstructor();
c.newInstance();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
运行结果如下:
结果中报的是java.lang.NoSuchMethodException异常,意思是没找到无参的构造方法。这时候, 我们打开java.lang.Enum的源码,查看它的构造方法,只有一个protected类型的构造方法,代码如 下:
代码语言:javascript复制 protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
然后我们将测试方法改一下:
代码语言:javascript复制public class EnumSingletonTest3 {
public static void main(String[] args) {
try {
Class clazz = EnumSingleton.class;
Constructor c = clazz.getDeclaredConstructor(String.class,int.class);
c.setAccessible(true);
EnumSingleton enumSingleton = (EnumSingleton) c.newInstance("zwq",666);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
结果变成了这样:
这时错误已经非常明显了,“Cannot reflectively create enum objects",即不能用反射来创建 枚举类型。我们还是习惯性地想来看看JDK源码,进入Constructor的newlnstance()方法︰
代码语言:javascript复制 @CallerSensitive
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()方法中做了强制性的判断,如果修饰符是Modifier.ENUM枚举类型,则直接抛出异常。这岂不是和静态内部类的处理方式有异曲同工之妙?对,但是我们自己再构造方法中写逻辑处理可能存在未知的风险,而JDK的处理是最官方、最权威、最稳定的。因此枚举式 单例模式也是《Effective Java》书中推荐的一种单例模式实现写法。 到此为止,我们是不是已经非常清晰明了呢?JDK枚举的语法特殊性及反射也为枚举保驾护航,让 枚举式单例模式成为一种比较优雅的实现。
2 容器式单例模式
其实枚举式单例,虽然写法优雅,但是也会有一些问题。因为它在类加载之时就将所有的对象初始化放在类内存中,这其实和饿汉式并无差异,不适合大量创建单例对象的场景。那么,接下来看注册式 单例模式的另一种写法,即容器式单例模式,创建ContainerSingleton类:
代码语言:javascript复制public class ContainerSingleton {
private ContainerSingleton(){}
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);
}
}
}
容器式单例模式适用于需要大量创建单例对象的场景,便于管理。但它是非线程安全的。到此,注 册式单例模式介绍完毕。我们有兴趣可以看看Spring中的容器式单例模式的实现代码︰
代码语言:javascript复制AbstractAutowireCapableBeanFactory
这个类里面可以看到符合单例模式的一切定义
线程单例实现ThreadLocal
ThreadLocal不能保证其创建的对象是全局唯一的,但是能保证在单个线程中是唯一的,天生是线程安全的。下面来看代码︰
代码语言:javascript复制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();
}
}
写个测试代码:
代码语言:javascript复制public class ThreadLocalSingletonTest {
public static void main(String[] args) {
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(ThreadLocalSingleton.getInstance());
Thread t1 = new Thread(new ExectorThread());
Thread t2 = new Thread(new ExectorThread());
t1.start();
t2.start();
System.out.println("End");
}
}
运行结果:
我们发现,在主线程中无论调用多少次,获取到的实例都是同一个,但是在两个子线程中分别获取到 了不同的实例。那么ThreadLocal是如何实现这样的效果的呢?我们知道,单例模式为了达到线程安全 的目的,会给方法上锁,以时间换空间。ThreadLocal将所有的对象全部放在ThreadLocalMap中, 为每个线程都提供一个对象,实际上是以空间换时间来实现线程隔离的。