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

反射破坏单例
上篇文章中介绍的单例模式的构造方法除了加上 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:
在这里插入图片描述
该种解决方式就是由于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());
    }
}

输出结果
在这里插入图片描述
不用做任何处理就已经预防了序列化对单例的破坏。为什么呢?
下载一个非常好用的 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();
}
}

在这里插入图片描述
意思是没找到无参的构造方法。我们打开 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();
        }
    }

在这里插入图片描述

告诉我们 Cannot reflectively create enum objects,不能用反射来创建枚举类型。看看 JDK 源码,进入 Constructor 的
newInstance()方法:
在这里插入图片描述
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 中,为每个线程都提供一个对象,实际上是以空间换时间来实现线程间隔离的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

catch that elf

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值