反射破坏单例
上篇文章中介绍的单例模式的构造方法除了加上 private 以外,没有做任何处理。如果我们使用反射来调用其构造方法,然后,再调用 getInstance()方法,应该就会两个不同的实例。现在来看一段测试代码,以 LazyInnerClassSingleton 为例

public class LazyInnerClassSingletonTest {
public static void main(String[] args) {
try{
Class<?> clazz = LazyInnerClassSingleton.class;
Constructor c = clazz.getDeclaredConstructor(null);
c.setAccessible(true);
Object o1 = c.newInstance();
Object o2 = c.newInstance();
System.out.println(o1 == o2);
}catch (Exception e){
e.printStackTrace();
}
}
}

输出结果为 false.显然,是创建了两个不同的实例。对于这种情况 我们可以在其构造方法中做一些限制,一旦出现多
次重复创建,则直接抛出异常。来看优化后的代码

public class LazyInnerClassSingleton {
private LazyInnerClassSingleton(){
if(LazyHolder.LAZY!=null){
throw new RuntimeException("不允许创建多个实例");
}
}
private LazyInnerClassSingleton lazy=null;
public static final LazyInnerClassSingleton getInstance(){
return LazyHolder.LAZY;
}
private static class LazyHolder{
private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
}
}

再次运行测试代码 多次创建报了异常

设计模式-单例模式(二)单例的破坏及高级实现_设计模式

序列化破坏单例

当我们将一个单例对象创建好,有时候需要将对象序列化然后写入到磁盘,下次使用时再从磁盘中读取到对象,反序列化转化为内存对象。反序列化后的对象会重新分配内存,即重新创建。那如果序列化的目标的对象为单例对象,就违背了单例模式的初衷,相当于破坏了单例,如下代码:

public class HungrySingleton implements Serializable {
private static final HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton(){}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}

public class SerialTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
HungrySingleton c1=null;
HungrySingleton c2=HungrySingleton.getInstance();
FileOutputStream fos = new FileOutputStream(new File("serializeobj"));
ObjectOutputStream oos= new ObjectOutputStream(fos );
oos.writeObject(c2 );
oos.flush();oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("serializeobj")));
Object o = ois.readObject();
ois.close();
c1=(HungrySingleton) o;
System.out.println(c1==c2);
}
}

输出结果false 。证明是两个不同的实例。反序列化后的对象和手动创建的对象是不一致的,实例化了两个对象,违背了单例的设计初衷。那么,我们如何保证序列化的情况下也能够实现单例?其实很简单,只需要增加 readResolve()方法即可。来看优化代码:

public class HungrySingleton implements Serializable {
private static final HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton(){}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
private Object readResolve(){
return hungrySingleton;
}
}

再次运行刚才的测试代码 结果为 true:

设计模式-单例模式(二)单例的破坏及高级实现_设计模式_02


该种解决方式就是由于java考虑到了对象实例化会对对象的单例产生影响而提供的。虽然这种方式解决了我们的问题,但是其实在源码中可以看到 ,新的实例其实已经被实例化,只不过实例化完成后又调用了 readResolve 方法,用该方法的返回结果替换新创建的实例返回。之前反序列化得到的新的实例会被gc回收。由于实例化了一个多余的对象,所以会消耗一定的内存资源,如果创建对象的动作发生频率增大,就意味着内存分配开销也就随之增大。所以这种方式还有优化的空间。

为什么不先判断readResolve是否存在并执行 ,这样就不会白创建对象了? 因为要调用方法必须有一个实例。所以要先把实例反序列化出来。**为啥不吧这个readResolve方法定义成 static的?出于内存效率考虑?**并且该方法返回的实例为static 的所以 所有实例都可以返回。)

注册式单例

注册式单例又称为登记式单例,就是将每一个实例都登记到某一个地方,使用唯一的标识获取实例。注册式单例有两种写法:一种为容器缓存,一种为枚举登记。先来看枚举式单例的写法,来看代码,创建 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 EnumSingletonTest {
public static void main (String[] args) throws Exception {
EnumSingleton ins1= null;
EnumSingleton ins2=EnumSingleton.getInstance();
ins2.setData(new Object());
FileOutputStream fos = new FileOutputStream("enumsingleton");
ObjectOutputStream oos = new ObjectOutputStream(fos );
oos.writeObject(ins2 );
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("enumsingleton");
ObjectInputStream ois = new ObjectInputStream(fis );
ins1=(EnumSingleton)ois.readObject();
ois.close();
System.out.println(ins1.getData());
System.out.println(ins2.getData());
System.out.println(ins2.getData()==ins1.getData());
}
}

输出结果

设计模式-单例模式(二)单例的破坏及高级实现_容器单例_03


不用做任何处理就已经预防了序列化对单例的破坏。为什么呢?

下载一个非常好用的 Java 反编译工具 Jad,反编译 class 目录的 EnumSingleton.class 类 可以看到如下代码

static
{
INSTANCE = new EnumSingleton("INSTANCE", 0);
$VALUES = (new EnumSingleton[] {
INSTANCE
});
}

原来,枚举式单例在静态代码块中就给 INSTANCE 进行了赋值,是饿汉式单例的实现。看一下 JDK源码,ObjectInputStream 的 readObject0()方法 。发现枚举类型其实通过类名和 Class 对象类找到一个唯一的枚举对象。因此,枚举对象不可能被类加载器加载多次。
测试 反射能否破坏,执行如下代码直接报异常

public static void main(String[] args) {
try {
Class clazz = EnumSingleton.class;
Constructor c = clazz.getDeclaredConstructor();
c.newInstance();
}catch (Exception e){
e.printStackTrace();
}
}

设计模式-单例模式(二)单例的破坏及高级实现_枚举单例_04


意思是没找到无参的构造方法。我们打开 java.lang.Enum 的源码代码,查看它的构造方法,只有一个 protected的构造方法,代码如下

protected Enum(String name, int ordinal) {
​​​this.name​​​ = name;
this.ordinal = ordinal;
}

我们调整执行如下代码,依然报异常

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("Tom",666);
}catch (Exception e){
e.printStackTrace();
}
}

设计模式-单例模式(二)单例的破坏及高级实现_反射破坏_05

告诉我们 Cannot reflectively create enum objects,不能用反射来创建枚举类型。看看 JDK 源码,进入 Constructor 的

newInstance()方法:

设计模式-单例模式(二)单例的破坏及高级实现_设计模式_06


newInstance()方法中做了强制性的判断,如果修饰符是 Modifier.ENUM 枚举类型,直接抛出异常。枚举式单例也是《EffectiveJava》书中推荐的一种单例实现写法。在 JDK 枚举的语法特殊性,以及反射也为枚举保驾护航,让枚举式单例成为一种比较优雅的实现。

容器缓存单例

代码如下:

public class ContainerSingleton {
private ContainerSingleton (){}
private static Map<String,Object> ioc = new ConcurrentHashMap<String,Object>();
public static Object getBean(String className){
synchronized(ioc){
if(!ioc.containsKey(className)){
Object obj=null;
try {
obj= Class.forName(className );
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
ioc.put(className,obj );
return obj;
}else{
return ioc.get(className );
}
}
}
}

容器式写法适用于创建实例非常多的情况,便于管理。但是,是非线程安全的

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 extends  Thread{

@Override
public void run() {
System.out.println(Thread.currentThread().getId()+"==="+ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread().getId()+"==="+ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread().getId()+"==="+ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread().getId()+"==="+ThreadLocalSingleton.getInstance());
}

public static void main(String[] args){
System.out.println(Thread.currentThread().getId()+"==="+ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread().getId()+"==="+ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread().getId()+"==="+ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread().getId()+"==="+ThreadLocalSingleton.getInstance());
Thread t1 = new Thread(new ThreadLocalSingletonTest());
Thread t2 = new Thread(new ThreadLocalSingletonTest());
t1.start();
t2.start();
}
}

结果:

1===com.test.singleton.register.ThreadLocalSingleton@19bb089b
1===com.test.singleton.register.ThreadLocalSingleton@19bb089b
1===com.test.singleton.register.ThreadLocalSingleton@19bb089b
1===com.test.singleton.register.ThreadLocalSingleton@19bb089b
16===com.test.singleton.register.ThreadLocalSingleton@530142c9
16===com.test.singleton.register.ThreadLocalSingleton@530142c9
16===com.test.singleton.register.ThreadLocalSingleton@530142c9
16===com.test.singleton.register.ThreadLocalSingleton@530142c9
14===com.test.singleton.register.ThreadLocalSingleton@823ae02
14===com.test.singleton.register.ThreadLocalSingleton@823ae02
14===com.test.singleton.register.ThreadLocalSingleton@823ae02
14===com.test.singleton.register.ThreadLocalSingleton@823ae02

在同一个线程中获取到的实例都是同一个。那么 ThreadLocal 是如果实现这样的效果的呢?我们知道上面的单例模式为了达到线程安全的目的,给方法上锁,以时间换空间。ThreadLocal将所有的对象全部放在 ThreadLocalMap 中,为每个线程都提供一个对象,实际上是以空间换时间来实现线程间隔离的。