赞
踩
本专栏内容参考自:咕泡学院Tom老师的《Spring5核心原理与30个类手写实战》,仅作个人学习记录使用,如有侵权,联系速删。
单例模式(Singleton Pattern )是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。单例模式是创建型模式。单例模式在现实生活中应用也非常广泛,例如,公司CEO、部门经理等。J2EE标准中的ServletContext,ServletContextConfig等、Spring框架应用中的ApplicationContext、数据库的连接池等也都是单例形式。
饿汉式单例模式在类加载的时候就立即初始化,并且创建单例对象。它绝对线程安全,在线程还没
出现以前就实例化了,不可能存在访问安全问题。
/**
* 优点:执行效率高,性能高,没有任何的锁
* 缺点:某些情况下,可能会造成内存浪费
*/
public class HungrySingleton {
private static final HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton(){}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
还有另外一种写法,利用静态代码块的机制:
public class HungryStaticSingleton { //先静态后动态 //先上,后下 //先属性后方法 private static final HungryStaticSingleton hungrySingleton; //装个B static { hungrySingleton = new HungryStaticSingleton(); } private HungryStaticSingleton(){} public static HungryStaticSingleton getInstance(){ return hungrySingleton; } }
这两种写法都非常简单,也非常好理解,饿汉式单例模式适用于单例对象较少的情况。这样写可以保证绝对线程安全、执行效率比较高。但是它的缺点也很明显,就是所有对象类加载的时候就实例化。
这样一来,如果系统中有大批量的单例对象存在,那系统初始化是就会导致大量的内存浪费。也就是说,
不管对象用与不用都占着空间,浪费了内存,有可能“占着茅坑不拉屎”。那有没有更优的写法呢?下
面我们来继续分析。
为了解决饿汉式单例可能带来的内存浪费问题,于是就出现了懒汉式单例的写法,懒汉式单例模式的特点是,单例对象要在被使用时才会初始化,下面看懒汉式单例模式的简单实现
LazySimpleSingleton :
/**
* 优点:节省了内存,线程安全
* 缺点:性能低
*/
public class LazySimpleSingletion {
private static LazySimpleSingletion instance;
private LazySimpleSingletion(){}
public static LazySimpleSingletion getInstance(){
if(instance == null){
instance = new LazySimpleSingletion();
}
return instance;
}
}
但这样写又带来了一个新的问题,如果在多线程环境下,就会出现线程安全问题。我先来模拟一下,
编写线程类ExectorThread :
public class ExectorThread implements Runnable{
@Override
public void run() {
LazySimpleSingletion instance = LazySimpleSingletion.getInstance();
LazyDoubleCheckSingleton instance = LazyDoubleCheckSingleton.getInstance();
}
}
客户端测试代码如下:
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分配压力上升,则会导致大批线程阻塞,从而导致程序性能大幅下降。那么,有没有一种更好的方式,既能兼顾线程安全又能提升程序性能呢?答案是肯定的。我们来看双重检查锁的单例模式∶
/** * 优点:性能高了,线程安全了 * 缺点:可读性难度加大,不够优雅 */ 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 关键字总归要上锁,对程序性能还是存在一定影响的。难道就真的没有
更好的方案吗﹖当然有。我们可以从类初始化的角度来考虑,看下面的代码,采用静态内部类的方式:
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为例:
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(); } }
再次运行,会得到以下结果:
至此,自认为史上最牛的单例模式的实现方式便大功告成,避免了线程问题,反射破坏,性能尚可,且写法优雅。但是,上面看似完美的单例写法还是有可能被破坏。
一个单例对象创建好后,有时候需要将对象序列化然后写入磁盘,下次使用时再从磁盘中读取对象并进行反序列化,将其转化为内存对象。反序列化后的对象会重新分配内存,即重新创建。如果序列化的目标对象为单例对象,就违背了单例模式的初衷,相当于破坏了单例,来看一段代码︰
public class SeriableSingleton implements Serializable { //序列化 //把内存中对象的状态转换为字节码的形式 //把字节码通过IO输出流,写到磁盘上 //永久保存下来,持久化 //反序列化 //将持久化的字节码内容,通过IO输入流读到内存中来 //转化成一个Java对象 public final static SeriableSingleton INSTANCE = new SeriableSingleton(); private SeriableSingleton(){} public static SeriableSingleton getInstance(){ return INSTANCE; } }
测试代码:
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()方法即可。来看优化后的代码︰
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()方法,
代码如下:
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()方法,让我们点进去:
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: %02X", tc)); } } finally { depth--; bin.setBlockDataMode(oldMode); } }
代码挺长,我们注意一下这一段
case TC_OBJECT:
if (type == String.class) {
throw new ClassCastException("Cannot cast an object to java.lang.String");
}
return checkResolve(readOrdinaryObject(unshared));
我们看到里面调用了readOrdinaryObject()方法,让我们再点进去看看:
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(),在这一段
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);
}
点进去,可以看到里面很简单:
boolean isInstantiable() {
requireInitialized();
return (cons != null);
}
判断了一下构造方法是否为空,不为空就返回true,这意味着只要有无参的构造方法,就会实例化。
然后再往上返回,回到ObjectInputStream的readOrdinaryObject()方法,找到这一块
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
我们可以看到代码里在判断无参构造方法之后,又调用了hasReadResolveMethod()这个方法,来看代码:
boolean hasReadResolveMethod() {
requireInitialized();
return (readResolveMethod != null);
}
就两三行,逻辑很简单,判断readResolveMethod 是否为空,不为空就返回true,那么readResolveMethod是在哪里赋值的呢?通过全局查找知道,在私有方法ObjectStreamClass()中给readResolveMethod进行了赋值,来看代码︰
//533行
readResolveMethod = getInheritableMethod(
cl, "readResolve", null, Object.class);
上面的逻辑其实就是通过反射找到一个无参的 readResolve()方法,并且保存下来。现在回到ObjectinputStream的readOrdinaryObject(方法继续往下看,如果readResolve()方法存在则调用invokeReadResolve()方法,来看代码︰
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()方法返回实例解决了单例模式被破坏的
问题,但是实际上实例化了两次,只不过新创建的对象没有被返回而已。如果创建对象的动作发生频率
加快,就意味着内存分配开销也会随之增大,难道真的就没办法从根本上解决问题吗﹖下面讲的注册式单例也许能帮助到你。
注册式单例模式又称为登记式单例模式,就是将每一个实例都登记到某一个地方,使用唯一的标识
获取实例。注册式单例模式有两种:一种为枚举式单例模式,另一种为容器式单例模式。
先来看枚举式单例模式的写法,来看代码,创建EnumSingleton类:
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; } }
写一下测试代码:
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文件我们惊奇地发现有
如下代码︰
static
{
INSTANCE = new EnumSingLeton("INSTANCE",0);
$VALUES = (new EnumSingleton[] {
INSTANCE
});
}
原来,枚举式单例模式在静态代码块中就给INSTANCE进行了赋值,是饿汉式单例模式的实现。至此,我们还可以试想,序列化能否破坏枚举式单例模式呢?不妨再来看一下JDK源码,还是回到ObjectInputStream的readObject0(方法∶找到这一块
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()方法的代码实现︰
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; }
我们发现,枚举类型其实通过类名和类对象类找到一个唯一的枚举对象。因此,枚举对象不可能被
类加载器加载多次。那么反射是否能破坏枚举式单例模式呢﹖来看一段测试代码︰
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类型的构造方法,代码如
下:
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
然后我们将测试方法改一下:
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()方法︰
@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枚举的语法特殊性及反射也为枚举保驾护航,让
枚举式单例模式成为一种比较优雅的实现。
其实枚举式单例,虽然写法优雅,但是也会有一些问题。因为它在类加载之时就将所有的对象初始化放在类内存中,这其实和饿汉式并无差异,不适合大量创建单例对象的场景。那么,接下来看注册式
单例模式的另一种写法,即容器式单例模式,创建ContainerSingleton类:
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中的容器式单例模式的实现代码︰
AbstractAutowireCapableBeanFactory
这个类里面可以看到符合单例模式的一切定义
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 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中,
为每个线程都提供一个对象,实际上是以空间换时间来实现线程隔离的。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。