你还不懂设计模式? - 单例模式

单例模式(Singleton Pattern),顾名思义,要确保Java类在任何情况都只有一个唯一的实例。看似简单,实际道道蛮多。下面来仔细分析下单例模式的几个实现方式。

  • 懒汉单例模式
public class LazySingleton {
    private LazySingleton(){
        System.out.println("LazySingleton is created !");
    }

    /**
     * 对于静态成员变量确保初始值为null,保证系统启动时不会有额外的负载。
     */
    private static LazySingleton instance = null ;

    private static  LazySingleton getInstance(){
        if (instance == null) {
            instance =  new LazySingleton();
        }
        return instance;
    }
}

在一般的后台管理系统中这样写完全OK,但是可以很清楚的看到,当并发上来之后,可能会重复创建这个对象。违反了单例的初衷。

改进下(方法上加synchronized关键字):

    private static synchronized LazySingleton getInstance(){
        if (instance == null) {
            instance =  new LazySingleton();
        }
        return instance;
    }

但是最为一个工作多年的Java开发人员。对于这种感觉还是不完美。因为虽然方法加锁能解决多线程问题,但是存在两个主要的问题:

① 当线程数量非常多时,一个线程获取到锁之后,synchronized 会阻塞其他所有线程,导致内存耗损集聚增加,程序性能下降。
② 假如在构建单例实例之前存在很多线程安全的操作,比如查询数据,这种在方法上直接加锁就太不友好了。还是浪费性能。

为了放在多线程等待的问题,可以使用双重检查锁DCL(Dobble Check Lock):

public class LazySingleton {
    private LazySingleton(){
        System.out.println("LazySingleton is created !");
    }

    /**
     *  使用volatile关键词,保证instance被线程赋值后能及时给别的线程可见
     */
    private volatile static LazySingleton instance = null ;

    private static LazySingleton getInstance(){
        //线程进来后第一判断是否为空。
        //这样可以防止当高并发时,线程一入方法就直接要去等待获取锁。使得当一个线程执行完实例创建时,别的线程无需再去获取锁
        if (instance == null) {
            //加锁,保证创建实例过程是线程安全的
            synchronized (LazySingleton.class){
                //最基础的判断,如果不存在单例就创建一个。
                if (instance == null){
                    /*
                     * 1 给待创建的LazySingleton实例分配内存
                     * 2 初始化对象实例
                     * 3 将instance指向instance
                     */
                    instance =  new LazySingleton();
                }
            }
        }
        return instance;
    }
}

到此这个懒汉单例模式就已经算是比较符合预期。在Dubbo框架源码中DCL这种单例模式出现的频率就非常高。

但是这种方法还是使用了synchronized关键字。那么可不可以不用synchronized来实现单例模式呢?答案是肯定的!

使用静态内部类的特性来实现懒汉模式:

public class LazySingleton {
    private LazySingleton() {
        System.out.println("LazySingleton is created !");
    }

    private static LazySingleton getInstance() {
        //使用静态内部类才会初始化
       return LazySingletonHolder.LAZY_SINGLETON;
    }

    private static class LazySingletonHolder {
        private final static LazySingleton LAZY_SINGLETON = new LazySingleton();
    }
}

测试一下:

public class LazySingleton {
    private LazySingleton() {
        System.out.println("LazySingleton is created !");
    }

    private static LazySingleton getInstance() {
        //使用静态内部类才会初始化LAZY_SINGLETON
       return LazySingletonHolder.LAZY_SINGLETON;
    }

    private static class LazySingletonHolder {
        static {
            System.out.println("开始初始化静态内部类啦");
        }
        private final static LazySingleton LAZY_SINGLETON = new LazySingleton();
    }
    public static void test(){
        System.out.println("123");
    }
    public static void main(String[] args) {
        LazySingleton.test();
        LazySingleton.getInstance();
    }
}
输出结果(达到预期):
123
开始初始化静态内部类啦
LazySingleton is created !
Process finished with exit code 0

这就完美利用了静态内部类在被调用的时候才会初始化的特点,巧妙了解决了线程的安全问题。可以算是懒汉单例模式一种很nice的写法。

但是这种方式也存在一个明显的问题,现在是通过类的构造函数设置private来防止被别的类实例化。Java中可以使用反射来修改构造方法的访问权限。如下:

public static void main(String[] args) throws Exception {
        Class<LazySingleton> lazySingletonClass = LazySingleton.class;
        Constructor<LazySingleton> constructor = lazySingletonClass.getDeclaredConstructor( );
        constructor.setAccessible(true);

        for (int i=0 ; i<5 ; i++){
            System.out.println(constructor.newInstance());
        }
    }
    输出结果:
    LazySingleton is created !
com.dongchao.designs.singleton.LazySingleton@3764951d
LazySingleton is created !
com.dongchao.designs.singleton.LazySingleton@4b1210ee
LazySingleton is created !
com.dongchao.designs.singleton.LazySingleton@4d7e1886
LazySingleton is created !
com.dongchao.designs.singleton.LazySingleton@3cd1a2f1
LazySingleton is created !
com.dongchao.designs.singleton.LazySingleton@2f0e140b

可以看到LazySingleton被实例化了5次,通过反射API强行修改类的构造函数的访问权限,可以破坏这种单例。

那能不能防止构造方法被多次调用呢?答案也是可以的,将上述代码做如下优化:

public class LazySingleton {
    private LazySingleton() {
        if (LazySingletonHolder.LAZY_SINGLETON !=null){
            throw new RuntimeException(String.format("不允许创建多个%s实例",LazySingleton.class));
        }
    }

    private static LazySingleton getInstance() {
        //使用静态内部类才会初始化LAZY_SINGLETON
       return LazySingletonHolder.LAZY_SINGLETON;
    }

    private static class LazySingletonHolder {
        static {
            System.out.println("静态内部类初始化啦!");
        }
        private final static LazySingleton LAZY_SINGLETON = new LazySingleton();
    }

    public static void main(String[] args) throws Exception {
        Class<LazySingleton> lazySingletonClass = LazySingleton.class;
        Constructor<LazySingleton> constructor = lazySingletonClass.getDeclaredConstructor( );
        constructor.setAccessible(true);

        for (int i=0 ; i<5 ; i++){
            System.out.println(constructor.newInstance());
        }
    }
}
输出结果:
静态内部类初始化啦!
Exception in thread "main" java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
	at com.dongchao.designs.singleton.LazySingleton.main(LazySingleton.java:34)
Caused by: java.lang.RuntimeException: 不允许创建多个class com.dongchao.designs.singleton.LazySingleton实例
	at com.dongchao.designs.singleton.LazySingleton.<init>(LazySingleton.java:12)
	... 5 more

可以看到,在构造方法中限制调用次数可以有效的解决反射暴力破坏单例的问题

  • 饿汉单例模式

饿汉模式,会在类初始化的时候就急不可耐的初始化好所需要的实例。在对于小的简单的对象是可以的,但是如果对于比较大比较重的对象来说,如果一开始就加载。用不用得上都会占用内存空间。有可能会浪费大量的内存。

下面来看一下实现:

public class SingleTon {
    private SingleTon(){
        //使用private访问级别的构造函数,防止在系统别的代码中被实例化。
        System.out.println("singleton is created !");
    }
    private final static SingleTon INSTANCE = new SingleTon();

    public static SingleTon getInstance(){
        return INSTANCE ;
    }
}

可以看到,实现很简洁,不需要加锁,执行效率比懒汉模式高。

在实际使用的,比如现在微服务架构中的RPC远程服务调用。会经常涉及到对象的序列化和反序列化。而序列化也是能够破坏单例的。下面来分析一下:

在这里插入图片描述
在将Java对象序列化为二进制文件流后,再反序列化回来。这个过程就很容易导致两个Java的对象实例不一致。

在Java中对象要是能被序列化,必须实现Serializable接口,并指定一个唯一的serialVersionUID属性

public class SingleTon implements Serializable{

    private static final long serialVersionUID = 5108241903043777229L;

    private SingleTon(){
        //使用private访问级别的构造函数,防止在系统别的代码中被实例化。
        System.out.println("singleton is created !");
    }
    private final static SingleTon INSTANCE = new SingleTon();

    public static SingleTon getInstance(){
        return INSTANCE ;
    }

    public static void main(String[] args) {
        SingleTon instance1 = SingleTon.getInstance();
        SingleTon instance2 = null ;
       try( ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\mypro\\java_study\\test"))){
            oos.writeObject(instance1);
       }catch (Exception ignored){
          ignored.printStackTrace();
       }
        try( ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\mypro\\java_study\\test"))){
            instance2 = (SingleTon) ois.readObject();
        }catch (Exception e){
           e.printStackTrace();
        }
        System.out.println("序列化之前:"+instance1);
        System.out.println("序列化之后:"+instance2);
        System.out.println(instance1 == instance2);
    }
}
输出结果:
singleton is created !
序列化之前:com.dongchao.designs.singleton.SingleTon@7440e464
序列化之后:com.dongchao.designs.singleton.SingleTon@27bc2616
false

可以看到序列化又重新产生了一个SingleTon实例,破坏了单例!

那有没解决方法呢?当然是有的,只需要在对象中新增方法readResolve()。

做如下优化:

public class SingleTon implements Serializable{

    private static final long serialVersionUID = 5108241903043777229L;

    private SingleTon(){
        //使用private访问级别的构造函数,防止在系统别的代码中被实例化。
        System.out.println("singleton is created !");
    }
    private final static SingleTon INSTANCE = new SingleTon();

    public static SingleTon getInstance(){
        return INSTANCE ;
    }
    private Object readResolve(){
        return INSTANCE;
    }
    public static void main(String[] args) {
        SingleTon instance1 = SingleTon.getInstance();
        SingleTon instance2 = null ;
       try( ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\mypro\\java_study\\test"))){
            oos.writeObject(instance1);
       }catch (Exception ignored){
          ignored.printStackTrace();
       }
        try( ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\mypro\\java_study\\test"))){
            instance2 = (SingleTon) ois.readObject();
        }catch (Exception e){
           e.printStackTrace();
        }
        System.out.println("序列化之前:"+instance1);
        System.out.println("序列化之后:"+instance2);
        System.out.println(instance1 == instance2);
    }
}
输出结果:
singleton is created !
序列化之前:com.dongchao.designs.singleton.SingleTon@7440e464
序列化之后:com.dongchao.designs.singleton.SingleTon@7440e464
true

可以看到新增readResolve()方法之后,确实能解决反序列化时返回一个新的对象的问题。但是在Java的源码实现中。还是重新创建了一个实例,只是原来的对象没有被返回。这只是表面上解决问题。实际还是会浪费内存和CPU性能。

  • 注册式单例模式
    在Java中有一种特殊的类,枚举 。 枚举类通常会被作为系统配置常量来使用。枚举类在序列化和反序列化时,Java源码中都有特殊处理,不会破坏原有结构。

下面是借助枚举来实现单例模式方式在阿里开源分布式事物解决方案seata源码中的应用:

public enum ObjectHolder {
    /**
     * singleton instance
     */
    INSTANCE;
    private static final int MAP_SIZE = 8;
    private static final Map<String, Object> OBJECT_MAP = new ConcurrentHashMap<>(MAP_SIZE);

    public Object getObject(String objectKey) {
        return OBJECT_MAP.get(objectKey);
    }

    public <T> T getObject(Class<T> clasz) {
        return clasz.cast(OBJECT_MAP.values().stream().filter(clasz::isInstance).findAny().orElseThrow(() -> new ShouldNeverHappenException("Can't find any object of class " + clasz.getName())));
    }

    public Object setObject(String objectKey, Object object) {
        return OBJECT_MAP.putIfAbsent(objectKey, object);
    }
}

这样巧妙的利用枚举类的特性,保证了ObjectHolder的单例。并解决了序列化的问题。这种方式可以被认为是现在最稳妥的单例解决方案。但是上面的其他的单例模式解决方案也可以看需求场景来使用。不能过于死板。敲代码不能过于追求完美!

关于Java序列化的问题涉及到的东西比较多,这也是Java语言本身的一个痛点。以后有时间我在深入研究!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值