java设计模式之单例模式详解

一、单例模式介绍

  1.1 什么是单例模式

  因为某些业务或代码逻辑需求,某些特定的类只能对其创建单一实例,用于实现这类需求的方法叫做单例模式。

  1.2 为什么要用单例

  ①对象只可以被实例化一次,意味创建此对象实例次数受到了控制,减少了系统内存消耗和大量创建实例产生的GC的消耗;

  ②对于某个类或某些通用服务(日志、打印等)需要被频繁创建实例的业务场景下,可以使用单例模式,减少代码耦合,清晰设计思路。

  1.3 单例模式的优缺点

  优点:

  ①正常情况下阻止其他对象实例化获得该实例副本,保证所有对象都仅能访问到该类的唯一实例;

  ②实例化过程由类完全控制,灵活性高。

  缺点:

  ①单例类实例化需要严格规范,容易产生不必要的内存开销或降低系统运行效率;

  ②除java之外的某些语言可以对实例进行写操作,容易出现悬浮引用情况;

  ③程序员编码过程中需要了解哪些是单例类,可能会有一点点开发效率上的影响(不会吧不会吧不会还有程序员不知道单例吧)?

二、饿汉、懒汉单例模式

  1、饿汉模式

  类在被加载时,就创建了该类的实例,代码如下:

public class StarvingSingleton {
    //静态final修饰,该变量将只会被初始化/赋值一次
    private static final StarvingSingleton starvingSingleton = new StarvingSingleton();
    private StarvingSingleton(){ }
    //静态方法,提供获取该类实例的入口
    public static StarvingSingleton getInstance(){
        return starvingSingleton;
    }

    public static void main(String[] args) {
        //打印查看两次获取实例的结果
        System.out.println(StarvingSingleton.getInstance());
        System.out.println(StarvingSingleton.getInstance());
    }
}

  饿汉单例模式中,构造方法使用private修饰符修饰,扼杀了使用构造方法创建实例的方式。在类加载的时候,静态变量得到加载,生成该类的实例。

  运行main方法之后:

  

   我们可以看到两次getInstance()方法获得的该类的实例是一样的。

 

  2、懒汉单例模式

  懒汉单例模式的设计思路,仅仅是在饿汉模式的基础上,将类的实例化过程延迟了,不再是类加载的时候直接实例化该类的对象。下面放代码:

public class LazySingleton {
    //声明实例变量但不对其进行初始化/实例化
    private static LazySingleton instance;
    //私有构造方法,防止通过构造实例化
    private LazySingleton(){}
    public static LazySingleton getInstance(){
        //判断是否已经存在该类实例,不存在则对该类进行实例化,返回已经存在的实例
        if (instance == null){
            instance = new LazySingleton();
        }
        return instance;
    }
}

  上面的代码当中,我们发现在类开头的实例变量并没有被初始化,我们把类的初始化放在类该类的静态方法getInstance()当中去实现。意味着我们需要在使用到这个方法的时候才会获取到该类的实例。又因为在getInstance()方法中增加了实例是否存在的判断,在某种程度上实现了单例。

 

三、饿汉单例模式可能出现的问题及其解决方案

  问题:

  1.饿汉单例模式在类加载时就实例化该类的对象,直接产生了内存消耗。(我们的希望更应该是在实例被使用的时候再产生内存消耗?)

  2. private修饰符修饰的构造方法可以被反射机制(Class.setAccessible(true)方法设置访问)强行使用,从而达到构造方法实例对象的效果。

  3. 通过序列化可以将创建出来的单例对象写入文件中,再通过反序列化创建实例,此时可能出现多个该类的实例。

  解决方式:

  使用枚举类。枚举类在能够保证线程安全的同时,也可以抵挡反射方式创建类实例,讲实例化的过程放在枚举类类型中,类加载的过程中,枚举类得到加载并且创建出该类的实例。

public class EnumStarvingSingleton {
    //私有构造
    private EnumStarvingSingleton(){}
    //获取实例的静态方法
    public static EnumStarvingSingleton getInstance(){
        return ContainerHolder.HOLDER.instance;
    }
    //枚举类型,初始化单例类的实例
    private enum ContainerHolder{
        HOLDER;
        private EnumStarvingSingleton instance;
        ContainerHolder(){
            instance = new EnumStarvingSingleton();
        }
    }
}

  此时,我们再使用反射方式并且setAccessible(true)之后,使用构造方法获取该类实例的话,将会得到一个IllegalArgumentException的异常,并提示Cannot reflectively create enum object的信息。

  对于序列化的产生的问题,留给大家自己查看。小tip:突破口在ObjectInputStream的readObject中,可以自己查看源码学习。

四、懒汉单例模式可能出现的问题及其解决方案

  问题:

  众所周知,多线程情况下,代码的执行顺序变得不可知了,在判断实例化对象是否存在的时候,有多个线程卡在这里,假如有两个线程同时进入,则有可能有多个实例产生,破坏单例的生态;

  解决方案:

  1.使用synchronized锁住该实例方法,但是synchronized属于重量级锁 ,直接对静态方法加锁,将整个类都锁住,大大影响了系统的性能(在此不做讲解)。

  2.使用双重检查锁机制,在获取实例方法的实例判断中锁住该类,再做一次实例是否存在的判断,代码如下:

public class LazyDoubleCheckSingleton {
    private static LazyDoubleCheckSingleton instance;
    private LazyDoubleCheckSingleton(){}
    public static LazyDoubleCheckSingleton getInstance(){
        //第一次检测
        if (instance==null){
            //在此处上锁,减少锁的消耗
            synchronized (LazyDoubleCheckSingleton.class){
                //第二次检测,此时只有可能有一条线程进入,不会出现代码乱序
                if (instance == null){
                    instance = new LazyDoubleCheckSingleton();
                }
            }
        }
        return instance;
    }
}

  那么,这么做就真的能够解决问题吗?答案是不一定!

  我们都知道,对象被创建的过程一般是分配内存空间,初始化对象,然后将内存地址赋值给变量。我们上面的代码中,new实例的过程中假如出现了重排序,还没有初始化对象的时候,先将内存地址赋值给了变量(这种情况是可能存在的),当线程B进入时,发现变量不为null,就会直接返回这个实例,然而此时可能拿到的是还没有初始化完成的对象。

  当然,别害怕,这个时候我们不能忘记了曾经学过的一个重要的关键字:voliate。voliate关键词的作用不光是保证线程修改可见,他还有一个关键的作用,就是一定程度上防止重排序的出现。当然,由于voliate也是一个重量级的关键词,在内存消耗上可能就不如预期了。对于这个关键词就不做过多的讲解,感兴趣的可以去查询相关资料。

   3. 使用静态内部类,应用静态内部类的特殊之处(外部类被加载的时候,不会产生内部类实例)做单例的实例化。

public class MySingleton {
    private MySingleton(){}
    //把实例化的任务交给内部类来做
    private static class SingletonHolder{
        public final static MySingleton instance = new MySingleton();
    }
    //获取该类实例
    public static MySingleton getInstance(){
        return SingletonHolder.instance;
    }
}

五、总结

  只有最适合自己需要的,才是最好的设计。

 

posted @ 2020-10-14 20:52  短歌行  阅读(125)  评论(0)    收藏  举报