vlambda博客
学习文章列表

设计模式专题——这次我们把单例模式讲清楚

每天进步一点点,坚持下去,你总是会不一样的。加油!


最近在整理java常用的一些基础、ZooKeeper、Spring全家桶、源码、Dubbo、Elasticsearch、Redis、MySql、RabbitMQ、Kafka、Linux 、微服务等技术栈。

持续更新,欢迎点上面后端架构进阶 关注!


今天开始我们开始学习设计模式相关的内容,今天的路线是先讲解下设计模式相关的知识点,比如设计原则等等,然后从我们的单例模式开始,具体举例子来完成对单例模式的讲解和学习。

以后我们先把目录给贴出来,心里有个数,然后更好的开始知识点的学习。

一、设计模式分类
二、设计模式六大原则
三、单例模式
四、单例模式优缺点
五、单例模式创建方式
六、单例模式的破坏和防治
七、小结

好了,我们开始今天的学习!

一、设计模式分类

创建型模式
共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。

结构型模式
共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。

行为型模式
共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

一共23种奉上!

二、设计模式六大原则


1、开闭原则

开闭原则:对扩展开发,对修改关闭。

在代码层面而言就是在你有新的需求的时候,你应当增加新的对象来实现,而不是修改原来的对象。


2、里氏代换原则

里氏代换原则:任何基类出现的地方,子类一定可以出现。

我将它称为父子代换原则,这也是为什么Java里面老是用接口或者抽象来作为参数然后传入的是子类或者实现的原因。因为接口与抽象存在的地方,都可以用它的实现或者子类来替换。其实这样也就实现了开闭原则。


3、依赖倒转原则

依赖倒转原则:针对接口编程,依赖抽象而不是具体类。

为什么要这样?因为这样的话就可以实现多样性。统一用接口“人”作为参数,统一调用“人”的speak()方法,传入的是中国人,日本日,美国人等不同的具体实例,那么speak()的表现形式就不同了,但是你又不需要修改调用代码,因为你是根据接口编程的。


4、接口隔离原则

接口隔离原则 :使用多个隔离的接口,降低接口的偶合。

简而言之就是单一责任原则:就一个类来说,它应当只做一件事情,只有一个引起它变化的原因。


5、迪米特法则

迪米特法则:一个实体应当尽量少的与其他实体之间发生相互作用。

这个也被称为最少知道原则。


6、合成复用原则

合成复用原则:尽量使用合成/聚合的方式,而不是使用继承。


三、单例模式

单例模式:保证一个类只有一个实例,并且提供一个访问该全局访问点。


四、单例模式优缺点

优点


1、提供了对唯一实例的受控访问;

2、由于在系统内存中只存在一个对象,因此可以节约系统资源,当需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能;

3、允许可变数目的实例;

4、避免对共享资源的多重占用。 


缺点


1、不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。

2、没有抽象层,因此单例类的扩展有很大的困难。


五、单例模式创建方式


饿汉式

类初始化时,会立即加载该对象,线程天生安全,调用效率高。


老规矩,上代码,走一波

  
    
    
  
/**
 * @description: 单例模式
 * @author:aerCool
 * @since:2020-03-07 23:16
 * @version:v1.0.0
 */

public class Singletone {

    //私有构造
    private Singletone() {}

    //直接创建好
    private static Singletone singletone = new Singletone();

    public static Singletone getInstance() {
        return singletone;
    }

    public static void main(String[] args) {

        Singletone single1 = Singletone.getInstance();
        Singletone single2 = Singletone.getInstance();

        System.out.println(single1 == single2);
    }
}


输出结果为: true


懒汉式

类初始化时,不会初始化该对象,真正需要使用的时候才会创建该对象,具备懒加载功能。

/**
 * @description: 单例模式
 * @author:aerCool
 * @since:2020-03-07 23:16
 * @version:v1.0.0
 */

public class Singletone {

    //私有构造
    private Singletone() {}

    //默认不初始化,使用的时候才去创建
    private static Singletone singletone;

    public static synchronized Singletone getInstance() {

        if (singletone == null) {
            singletone = new Singletone();
        }

        return singletone;
    }

    public static void main(String[] args) {
        Singletone single1 = Singletone.getInstance();
        Singletone single2 = Singletone.getInstance();

        System.out.println(single1 == single2);
    }
}


输出结果为:true


静态内部方式

结合了懒汉式和饿汉式各自的优点,真正需要对象的时候才会加载,加载类是线程安全的。

/**
 * @description: 单例模式
 * @author:aerCool
 * @since:2020-03-07 23:16
 * @version:v1.0.0
 */

public class Singletone {

    //私有构造
    private Singletone() {}

    //静态内部类
    private static class ClassInstance {
        private static final Singletone instance = new Singletone();
    }

    public static Singletone getInstance() {
        return ClassInstance.instance;
    }

    public static void main(String[] args) {
        Singletone single1 = Singletone.getInstance();
        Singletone single2 = Singletone.getInstance();

        System.out.println(single1 == single2);
    }
}


输出结果是:true


优势: 兼顾了懒汉模式的内存优化(使用时才初始化)以及饿汉模式的安全性(不会被反射入侵)。
 
劣势:需要两个类去做到这一点,虽然不会创建静态内部类的对象,但是其 Class 对象还是会被创建,而且是属于永久带的对象。

枚举单例
枚举实现单例模式, 实现简单、调用效率高 ,枚举本身就是单例,由jvm从根本上提供保障,避免通过反射和反序列化的漏洞。(推荐)

/**
 * @description: 单例模式
 * @author:aerCool
 * @since:2020-03-07 23:16
 * @version:v1.0.0
 */

public class Singletone {

    private enum SingleEnum {
        INSTANCE;
        private Singletone singletone;

        SingleEnum() {
            singletone = new Singletone();
        }

        public Singletone getInstance() {
            return singletone;
        }
    }

    public static void main(String[] args) {
        Singletone single1 = SingleEnum.INSTANCE.getInstance();
        Singletone single2 = SingleEnum.INSTANCE.getInstance();

        System.out.println(single1 == single2);
    }
}


双重检测锁方式 
(因为JVM本质重排序的原因,可能会初始化多次,不推荐使用)

/**
 * @description: 单例模式
 * @author:aerCool
 * @since:2020-03-07 23:16
 * @version:v1.0.0
 */

public class Singletone {

    private static Singletone singletone;

    public static Singletone getInstance() {

        if (singletone == null) {
            synchronized (Singletone.class) {
                if (singletone == null) {
                    singletone = new Singletone();
                }
            }
        }
        return singletone;
    }

    public static void main(String[] args) {
        Singletone single1 = Singletone.getInstance();
        Singletone single2 = Singletone.getInstance();

        System.out.println(single1 == single2);
    }
}


输出结果是:true,性能上肯定不如枚举和饿汉式。

六、单例模式的破坏和防治


破环单例模式的三种方式:反射,序列化,克隆


我们用上面的双重检测锁方式演示一下反射创建实例


/**
 * @description: 单例模式
 * @author:aerCool
 * @since:2020-03-07 23:16
 * @version:v1.0.0
 */

public class Singletone {

    private static Singletone singletone;

    public static Singletone getInstance() {

        if (singletone == null) {
            synchronized (Singletone.class) {
                if (singletone == null) {
                    singletone = new Singletone();
                }
            }
        }
        return singletone;
    }

    public static void main(String[] args) {

        Singletone single1 = Singletone.getInstance();
        System.out.println("single1 的hashcode : "+ single1.hashCode());

        //反射创建实例
        try {
            Constructor<Singletone> constructor = Singletone.class.getDeclaredConstructor();
            constructor.setAccessible(true);
            Singletone single2 = constructor.newInstance();
            System.out.println("single2 的hashcode : "+ single2.hashCode());
        }catch (Exception e) {

        }
    }
}


输出结果如下,可见被破坏了:


single1 的hashcode : 3798928
single2 的hashcode : 31958635


七、如何防止反射、克隆、序列化对单例模式的破环


反射破环(虽然构造方法已私有化,但通过反射机制使用newInstance()方法构造方法可以创建实例)

  • 首先定义一个全局变量开关createFlag默认为true

  • 当第一次创建实例时将其状态更改为false

克隆破环

  • 重写clone(),直接返回单例对象

序列化破环

  • 添加readResolve(),返回Object对象


 @Override
  protected Singleton clone() throws CloneNotSupportedException {
      return singleton;
  }
  private Object readResolve() {
      return singleton;
  }


七、小结

如果不需要延迟加载单例,可以使用枚举或者饿汉式,相对来说枚举性能高于饿汉式。


如果需要延迟加载,可以使用静态内部类或者懒汉式,相对来说静态内部类性能高于懒汉式。最好使用饿汉式 。 



好了,以上就是今天的内容。有错误的地方欢迎指正。快分享给你的朋友,一起学习吧!

靓仔,点个"在看"支持一下