zoukankan      html  css  js  c++  java
  • 单例设计模式

    一、概述

    1、什么是单例设计模式?

    在某些特殊场合中,一个类只能够产生一个实例对象,并且这个实例对象要可以对外提供访问。这样的类叫做单例类, 而设计单例的流程和思想叫做单例设计模式

    单例模式属于设计模式三大类中的创建型模式

    2、单例设计模式的特点

    单例模式具有典型的三个特点:

    • 只有一个实例。
    • 自我实例化。
    • 提供全局访问点。

    注意:

    注:注意单例模式所属类的构造方法是私有的,所以单例类是不能被继承的。 (这句话表述的有点问题,单例类一般情况只想内部保留一个实例对象,所以会选择将构造函数声明为私有的,这才使得单例类无法被继承。单例类与继承没有强关联关系。)

    3、单例设计模式的UML类图

    单例模式的UML结构图非常简单,就只有一个类,如下图:

    clipboard

    Singleton类,定义一个静态方法,getInstance(),可以通过类名来调用,主要负责替代构造方法,创建Singleton类唯一的实例对象。

    这个类可以对外提供访问,允许用户通过getInstance()方法访问它唯一的实例。

    4、单例设计模式的优缺点:

    优点:

    1)、由于单例模式只生成了一个实例,所以能够节约系统资源,减少性能开销,提高系统效率,

    2)、避免频繁的创建销毁对象,可以提高性能;

    3)、避免对共享资源的多重占用,简化访问;

    4)、为整个系统提供唯一一个全局访问点,能够严格控制客户对它的访问。

    缺点:

    1)、不适用于变化频繁的对象;

    2)、也正是因为系统中只有一个实例,这样就导致了单例类的职责过重,违背了“单一职责原则”,

    滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;

    3)、同时也没有抽象类,这样扩展起来有一定的困难。

    4)、如果实例化的对象长时间不被利用,系统会认为该对象是垃圾而被回收,这可能会导致对象状态的丢失;(这个所有的对象都会,跟单例无关。)

    5、单例模式的应用场景:

    场景一:

    windows的任务管理器,无论你点击多少次,始终都只有一个管理器窗口存在,系统并不会为你创建新的窗口,也就是说,整个系统运行的过程中,系统只维护了一个进程管理器的实例。这就是一个典型的单例模式运用。

    场景二:

    线程池、数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,用单例模式来维护,就可以大大降低这种损耗。

    场景三:

    程序的日志模块。一般也是采用单例模式实现。由于共享的日志文件一直处于打开状态,只能有一个实例去操作,否则内容不好追加。 采用单例模式就可以。

    场景四:

    Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。

    这些配置信息存放在一个文件中,由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。

    场景五:

    在我们的实际项目开发中,可以使用单例模式来封装一些常用的工具类,保证整个应用常用的数据统一。或者保存一些共享数据在内存中,其他类随时可以读取。

    二、单例模式的实现步骤

    可以使用如下的步骤实现一个单例类:

    单例设计模式的实现流程

    1、将构造方法私有化,使用private关键字修饰。使其不能在类的外部通过new关键字实例化该类对象。
    
    2、在该类内部产生一个唯一的实例化对象,并且将其封装为private static类型。
    
    3、对外提供一个静态方法getInstance()负责将对象返回出去,使用public static修饰
    

    三、单例模式的实现方式 (推荐枚举类方式)

    1、饿汉式——立即加载

    线程安全,调用效率高。但是不能延时加载。

    立即加载就是加载类的时候就已经将对象创建完毕(不管以后会不会使用到该实例化对象,先创建了再说。很着急的样子,故又被称为“饿汉模式”),常见的实现办法就是直接new实例化。

    所以加载类的速度比较慢,但是获取对象的速度比较快,且是线程安全的。

    /**
     * 饿汉式 
     */
    public class Singleton {
    
        // 创建全局唯一的实例化对象,在类初始化时,就会立即加载这个对象
        private static Singleton instance = new Singleton();
    
        // 私有化构造方法
        private Singleton() {}
    
        // 提供公有静态方法返回对象
        public static Singleton getInstance() {
            return instance;
        }
    }

    我们知道,类加载的方式是按需加载,且加载一次。因此,在上述单例类被加载时,就会实例化一个对象并交给自己的引用,供系统使用;而且,由于这个类在整个生命周期中只会被加载一次,因此只会创建一个实例,即能够充分保证单例。

    优缺点:

    优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。

    缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。(因为这个static的instance对象会一直占着这段内存,直到卸载类(即便你还没有用到这个实例))

    2、懒汉式——延迟加载

    延迟加载就是调用get()方法时实例才被创建(先不急着实例化出对象,等要用的时候才给你创建出来。不着急,故又称为“懒汉模式”),常见的实现方法就是在get方法中进行new实例化。

    /**
     * 懒汉式
      */
    public class Singleton {
        
        // 声明一个自身实例对象的引用
        private static Singleton instance;
        
        // 私有化构造方法
        private Singleton(){}
        
        // 提供公有静态方法返回对象
        public static Singleton getInstance() {
            // 判断如果为空,就创建,如果已经有了,就直接返回该实例,避免重复创建,保证全局唯一
            if (instance == null) {
                instance = new Singleton();
            }
            return instance;
        }
    }

    由于该模式是在运行时加载对象的,所以加载类比较快,但是对象的获取速度相对较慢,且线程不安全。如果想要线程安全的话可以加上synchronized关键字,但是这样会付出惨重的效率代价。

    我们从懒汉式单例可以看到,单例实例被延迟加载,即只有在真正使用的时候才会实例化一个对象并交给自己的引用。

    这种写法起到了Lazy Loading的效果,但是只能在单线程下使用。如果在多线程下,一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式。

    “懒汉模式”的优缺点:

    优点:实现起来比较简单,当类SingletonTest被加载的时候,静态变量static的instance未被创建,只是声明了一个引用,并未分配内存空间。要当getInstance方法第一次被调用时,初始化instance变量,才会真正创建对象,开始分配内存,因此在某些特定条件下会节约了内存。(需要时才创建)

    缺点:在多线程环境中,这种实现方法是完全错误的,根本不能保证单例的状态。

    3、线程安全的“懒汉模式”——  synchronized

    在懒汉模式的基础上,增加了synchronized锁同步机制,保证全局唯一。

    /**
     * 3、线程安全的懒汉式 —— synchronized
     */
    public class Singleton {
    
        // 声明一个自身实例对象的引用
        private static Singleton instance;
    
        // 私有化构造方法
        private Singleton(){}
    
        // 提供公有静态方法返回对象,加上synchronized关键字实现同步
        public static synchronized Singleton getInstance() {
            // 判断如果为空,就创建,如果已经有了,就直接返回该实例,避免重复创建,保证全局唯一
            if (instance == null) {
                instance = new Singleton();
            }
            return instance;
        }
    }

    优点:在多线程情形下,保证了“懒汉模式”的线程安全。

    缺点:众所周知在多线程情形下,synchronized方法通常效率低,显然这不是最佳的实现方案。

    4、懒汉式(DCL双重检测锁)

    DCL双检查锁机制(DCL:double checked locking)

    /**
     * 4、懒汉式 —— DCL双重检查锁机制(类锁)
     * 再一次缩小了锁的范围,提供了性能
     */
    public class Singleton {
    
        // 声明一个自身实例对象的引用,使用volatile保证多线程下引用的一致性
        private static volatile Singleton instance;
    
        // 私有化构造方法
        private Singleton(){}
    
        // 提供公有静态方法返回对象
        public static Singleton getInstance() {
            // 第一次检查instance是否被实例化出来,如果没有,再加锁处理
            if (instance == null) {
                synchronized (Singleton.class) {
                    // 某个线程取得了类锁,实例化对象前第二次检查instance是否已经被实例化出来,如果没有,才最终实例出对象
                    if (instance == null) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }

    Double-Check概念对于多线程开发者来说不会陌生。

    我们这里相比3直接对静态方法getInstance加上synchronized锁的方式,缩小了锁的范围。

    将第一个if判断块释放出来了,如果实例存在,则根本不会锁住,大大加快了返回实例的效率。

    只有当第一次if检查后,确定实例是真的不存在,需要创建时,此时才会开始加锁,注意此时加的是类锁,不是对象锁。不过这里是static静态方法,对象也是静态的,所以实际上它们效果是一样的。


    为什么在锁里面还要再次判定是否为空呢?

    因为高并发,后面的线程在第一次判定实例时也为空,也可以获得锁,只是要排队,只是在等待前面的线程释放锁。所以,当轮到它拿到锁之后,可能前面的线程已经创建了实例,所以要再次判定是否为空。这样才能保证实例唯一。

    (重点在于,多个线程可以同时通过第一个if,然后都可以按顺序执行锁里的代码。)


    Java指令重排的问题

    注意:单纯使用上面这种方式,仍然是线程不安全的。

    因为存在java指令重排的问题。

    在java创建对象的时候,cpu按照以下三个步骤来执行:

    1、memory = allocate() 在堆内存中开辟对象的内存空间,并指定地址

    2、根据类加载的顺序,初始化对象。

    3、instance = memory 设置instance指向刚分配的内存地址。instance是变量,存在栈中。


    单纯执行以上三步没啥问题,但是在多线程情况下,可能会发生指令重排序。

    指令重排序对单线程没有影响,单线程下CPU可以按照顺序执行以上三个步骤,但是在多线程下,如果发生了指令重排序,则会打乱上面的三个步骤。

    如果发生了JVM和CPU优化,发生重排序时,可能会按照下面的顺序执行:

    1、memory = allocate() 在堆内存中开辟对象的内存空间,并指定地址

    3、instance = memory 设置instance指向刚分配的内存地址。instance是变量,存在栈中。

    2、根据类加载的顺序,初始化对象。


    假设目前有两个线程A和B同时执行getInstance()方法,

    • A线程执行到instance = new Singleton(); B线程刚执行到第一个 if (instance == null) 处,
    • 如果按照1.3.2的顺序,假设线程A执行到第三步3.instance = memory 设置instance指向刚分配的内存,此时,线程B判断instance已经有值,就会直接return instance;
    • 而实际上,线程A还未执行第二步 初始化对象,也就是说线程B拿到的instance对象还未进行初始化,这个未初始化的instance对象一旦被线程B使用,就会出现问题。

    5、懒汉式(DCL双重检测锁机制+volatile禁止指令重排)—— 推荐

    相比4,这里对引用加入了volatile机制,禁止java的指令重排

    懒汉式的单例模式的最佳实现方式。内存消耗少,效率高,线程安全,多线程操作原子性。

    /**
     * 5、懒汉式 —— DCL双重检查锁机制(类锁) + volatile禁止指令重排
     * 再一次缩小了锁的范围,提供了性能。(推荐)
     */
    public class Singleton {
    
        // 声明一个自身实例对象的引用,使用volatile禁止指令重排,保证多线程下引用的一致性
        private static volatile Singleton instance;
    
        // 私有化构造方法
        private Singleton(){}
    
        // 提供公有静态方法返回对象
        public static Singleton getInstance() {
            // 第一次检查instance是否被实例化出来,如果没有,再加锁处理
            if (instance == null) {
                synchronized (Singleton.class) {
                    // 某个线程取得了类锁,实例化对象前第二次检查instance是否已经被实例化出来,如果没有,才最终实例出对象
                    if (instance == null) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }

    线程安全;延迟加载;效率较高。

    6、静态代码块——立即加载

    静态代码块方式跟饿汉式的方式几乎是一样的,只是把初始化代码放到了static块中了。

    因为我们知道,类加载的时候,这些属性和静态代码块都是会跟随类一起加载的,所以它的实现方式和饿汉式一样。也是线程安全的。

    /**
     * 6、静态代码块方式
     * 方式类似饿汉式,也是立即加载,是线程安全的
      */
    public class Singleton {
    
        // 在外部声明一个对象的引用,注意不能放到静态代码块中
        private static Singleton instance;
    
        // 静态代码块中,创建唯一实例对象,赋值给引用。
        static {
            instance = new Singleton();
        }
    
        // 私有化构造方法
        private Singleton() {}
    
        // 提供公有静态方法返回实例对象
        public static Singleton getInstance() {
            return instance;
        }
    }

    优缺点:

    优缺点都同饿汉式一样,也是立即加载,线程安全的。

    这里定义静态变量时要注意:

    静态变量只能定义在类的内部,不可以定义在静态块或方法中。可以在类内部定义静态变量,在静态块中进行初始化操作,因为类的内部是不允许有操作语句存在的,比如JDBC操作,所以可以在静态块static{} 中进行初始化操作,如:JDBC 定义静态变量主要是为了供外部访问,定义在一个局部中外部没有权限访问,为什么要定义呢,而且不能定义。

    7、静态内部类

    懒汉模式需要考虑线程安全,所以我们多写了好多的代码,饿汉模式利用了类加载的特性为我们省去了线程安全的考虑,那么,既能享受类加载确保线程安全带来的便利,又能延迟加载的方式,就是静态内部类。Java静态内部类的特性是,加载的时候不会加载内部静态类,使用的时候才会进行加载。而使用到的时候类加载又是线程安全的,这就完美的达到了我们的预期效果~

    /**
     * 7、静态内部类
     * 融合饿汉式和懒汉式的优点,推荐
      */
    public class Singleton {
    
        // 私有静态内部类中创建并初始化实例对象,注意要private私有化,不能被外部调用了
        private static class SingletonInner{
            private static Singleton instance = new Singleton();
        }
    
        // 私有化构造方法
        private Singleton() {}
    
        // 提供公有静态方法,返回实例对象
        public static Singleton getInstance() {
            return SingletonInner.instance;
        }
    }

    似乎静态内部类看起来已经是最完美的方法了,其实不是,可能还存在反射攻击或者反序列化攻击。


    8、枚举类 —— 线程最安全(最佳方式)

    单元素的枚举类型已经成为实现Singleton的最佳方法

                          -- 出自 《effective java》

    在effective java(这本书真的很棒)中说道,最佳的单例实现模式就是枚举模式。利用枚举的特性,让JVM来帮我们保证线程安全和单一实例的问题。除此之外,写法还特别简单。

    /**
     * 8、枚举类
     * 最佳实现方式
      */
    public enum Singleton {
        INSTANCE;
    }
    注意:

    因为INSTANCE实例是public公有的,可以直接通过类名的方式调用,即Singleton.INSTANCE,

    就不再需要提供公有静态方法getInstance()来返回对象了。


    这是最简洁、最安全的方式,不过它不能实现lazy loading延迟加载。


    其实枚举类它本身就具备单例的特性:

    比如:都会私有化构造方法。枚举类会对属性值加上public static final的属性,保障这个属性值都是全局唯一的。这些操作都和单例很像

    所以把这个属性变成对象,它就是一个单例类。

    类似于这种内部类的形式:

    public class Singleton {
        public static final Singleton INSTANCE = new Singleton();
    }

    枚举类继承自ENUM,内部实现了Serializable接口,所以不用考虑序列化的问题(其实序列化反序列化也能导致单例失败的,但是我们这里不过多研究)。

    对于线程安全,同样的,加载的时候JVM能确保只加载一个实例。避免暴力反射创建多个实例,绝对防止多次实例化。


    枚举类最佳实践:

    参考:https://www.jianshu.com/p/d35f244f3770

    枚举单例示例:

    public enum  EnumSingleton {
        INSTANCE;
        public EnumSingleton getInstance(){
            return INSTANCE;
        }
    }

    实际应用场景中,很多人会这么使用枚举单例:

    public class User {
        //私有化构造函数
        private User(){ }
     
        //定义一个静态枚举类
        static enum SingletonEnum{
            //创建一个枚举对象,该对象天生为单例
            INSTANCE;
            private User user;
            //私有化枚举的构造函数
            private SingletonEnum(){
                user=new User();
            }
            public User getInstnce(){
                return user;
            }
        }
     
        //对外暴露一个获取User对象的静态方法
        public static User getInstance(){
            return SingletonEnum.INSTANCE.getInstnce();
        }
    }
    
    public class Test {
        public static void main(String [] args){
            System.out.println(User.getInstance());
            System.out.println(User.getInstance());
            System.out.println(User.getInstance()==User.getInstance());
        }
    }
    结果为true

    以上代码看起来已经是ok了,其实不是,可能还存在反射攻击或者反序列化攻击

    最终版

    public enum Singleton {
    
        INSTANCE;
    
        public void doSomething() {
            System.out.println("doSomething");
        }
    
    }
    
    // 调用方法:
    
    public class Main {
    
        public static void main(String[] args) {
            Singleton.INSTANCE.doSomething();
        }
    
    }
    
    // 直接通过Singleton.INSTANCE.doSomething()的方式调用即可。方便、简洁又安全。

    推荐大家使用枚举类实现单例模式。

    四、各种实现方式的选择

    一般情况下,懒汉式(包含线程安全和线程不安全两种方式)都比较少用;

    饿汉式和DCL双重检测锁都可以使用,可根据具体情况自主选择;

    在要明确实现 lazy loading 效果时,可以考虑静态内部类的实现方式;

    若涉及到反序列化创建对象时,大家也可以尝试使用枚举方式。

    在选择时,请参考下面这张图:

    图片来源:https://www.cnblogs.com/rainbowbridge/p/12902359.html

    clipboard

    五、破坏单例模式的方法及解决办法

    参考:https://blog.csdn.net/b_just/article/details/104061314

    1、除枚举方式外, 其他方法都会通过反射的方式破坏单例,反射是通过调用构造方法生成新的对象,所以如果我们想要阻止单例破坏,可以在构造方法中进行判断,若已有实例, 则阻止生成新的实例,解决办法如下:

    private SingletonObject1(){
        if (instance !=null){
            throw new RuntimeException("实例已经存在,请通过 getInstance()方法获取");
        }
    }

    2、如果单例类实现了序列化接口Serializable, 就可以通过反序列化破坏单例,所以我们可以不实现序列化接口,如果非得实现序列化接口,可以重写反序列化方法readResolve(), 反序列化时直接返回相关单例对象。

    public Object readResolve() throws ObjectStreamException {
        return instance;
    }


    引用转载:

    https://www.jianshu.com/p/3f5eb3e0b050 (爆赞)

    https://www.cnblogs.com/xuwendong/p/9633985.html (爆赞)

    https://segmentfault.com/a/1190000010755849 (赞)

    https://www.cnblogs.com/binaway/p/8889184.html

    https://www.jianshu.com/p/d35f244f3770 (赞)

    https://blog.csdn.net/b_just/article/details/104061314

  • 相关阅读:
    五.Flink实时项目电商用户行为分析之订单支付实时监控
    四.Flink实时项目电商用户行为分析之恶意登录监控
    三.Flink实时项目电商用户行为分析之市场营销商业指标统计分析
    二.Flink实时项目电商用户行为之实时流量统计
    一.Flink实时项目电商用户行为分析之实时热门商品统计
    Flink 流处理API之实现UDF函数——更细粒度的控制流
    二.Flink 流处理API之Transform
    5组-Alpha冲刺-1/6
    5组 需求分析报告
    5组 团队展示
  • 原文地址:https://www.cnblogs.com/doublexi/p/15701024.html
Copyright © 2011-2022 走看看