zoukankan      html  css  js  c++  java
  • 单例模式-最简单的设计模式?

    一.说在前面

    在系统开发设计中,总会存在这么几种情况,①需要频繁创建销毁的对象,②创建对象需要消耗很多资源,但又经常用到的对象(如工具类对象,频繁访问数据库或文件的对象,数据源,session工厂等);③某个类只能有一个对象,如应用中的Application类;这时就应该考虑使用单例模式。个人博客地址www.mycookies.cn

    二.单例模式的动机

    • 在软件系统中,经常有一些特殊的类,必须保证他们在系统中只存在一个实例,才能保证他们的逻辑正确性,以及良好的效率
    • 如何绕过常规的构造器,提供以中机制保证一个类只有一个实例?这应该是类设计的责任,而不是使用者的责任

    三.模式定义

    确保类只有一个实例,并提供全局访问点。

    饿汉式

    在类初始化完成之后就完成了对象创建, 无论是否使用都要提前进行实例化。

    /**
     * 饿汉式[工厂方法]
     *
     * @author Jann Lee
     * @date 2019-07-21 14:28
     */
    public class Singleton1 {
    
        private static final Singleton1 instance = new Singleton1();
    
        private Singleton1() {
        }
    
        public static Singleton1 getInstance() {
            return instance;
        }
    }
    
     /**
     * 饿汉式[公有域]
     */
    public class Singleton2 {
        
        public static final Singleton2 instance = new Singleton2();
        
        private Singleton2() {
        }
    }
     /**
     * 饿汉式[静态代码块,工厂方法]
     */
    public class Singleton3 {
    
        private static Singleton3 instance;
    
        static {
            instance = new Singleton3();
        }
    
        private Singleton3() {
        }
    
        public static Singleton3 getInstance() {
            return instance;
        }
    }
    

    以上三种写法仅仅是在代码实现上的差异,是“饿汉式”最常见的实现。

    优点:类装在时候完成实例化,避免了多线程问题
    缺点:可能造成内存浪费(可能实例从来没有被使用到)

    懒汉式

    顾名思义,因为懒,所以在用到时才会进行实例化。

    实现思路:私有化构造方法-> 声明成员变量 -> 提供公共方法访问【如果成员变量不为空,直接返回,如果为空创建后返回】

    /**
     * 1.懒汉式[非线程安全]
     *
     * @author Jann Lee
     * @date 2019-07-21 14:31
     **/
    public class Singleton1 {
    
        private static Singleton1 instance;
    
        private Singleton1() {
        }
    
        public static Singleton1 getInstance() {
            if (instance == null) {
                instance = new Singleton1();
            }
            return instance;
        }
    }
    

    上述实现方式是最容易被想到的,但是应该也算是一种错误的实现方式,因为再多线程环境下一个对象可能被创建了多次。

    为了解决线程安全问题, 将方法进行同步

    /**
     * 2.懒汉式[同步方法]
     **/
    public class Singleton2 {
    
        private static Singleton2 instance;
    
        private Singleton2() {
        }
        
       /**
         * 同步方法,保证线程安全
         */
        public static synchronized Singleton2 getInstance() {
            if (instance == null) {
                instance = new Singleton2();
            }
            return instance;
        }
    }
    

    的确,这样实现解决了并发情况下带来的问题。但是每次获取对象都需要进行同步,然而对象创建只需要进行一次即可;因为并发读取数据不需要进行同步,所以这种在方法进行同步是没有必要的,在并发情况下肯定会带来性能上的损失。

    这时我们首先想到的时缩小同步范围,只有在创建对象的时候使用同步,即使用同步代码块实现。

    /**
     * 4.懒汉式[同步代码块]
     **/
    public class Singleton3 {
    
        private static Singleton3 instance;
    
        private Singleton3() {
        }
    
        /**
         * 同步代码块,并不能保证线程安全
         */
        public static Singleton3 getInstance() {
            if (instance == null) {
                synchronized (Singleton3.class) {
                    instance = new Singleton3();
                }
            }
            return instance;
        }
    }
    

    这是一种错误的实现方式,虽然减少了加锁范围,但是又回到了并发环境下的重复创建对象的问题,具体分析思路可以参考上文中的图。两个线程在if (instance==null)之后发生了竞争,线程1获取到了锁,线程2挂起,执行完毕后释放锁,此时线程二获取到锁后回接着往下执行,再次创建对象。

    解决上述问题,我们只需要在同步代码块中加入校验。

    /**
     * 懒汉式[双重检查锁]
     **/
    public class Singleton4 {
    
        private static volatile Singleton4 instance;
    
        private Singleton4() {
        }
    
        public static Singleton4 getInstance() {
            if (instance == null) {
                synchronized (Singleton4.class) {
                    if (instance == null) {
                        instance = new Singleton4();
                    }
                }
            }
            return instance;
        }
    }
    

    注意:此处除了再次校验是否为空之外,还给成员变量之前加上了一个volatile关键字,这个非常重要。因为在代码执行过程中会发生指令重排序。这里你只需要知道加上volatile之后能保证指令不会被重排序,程序能正确执行,而不加则可能不出错。

    在编译器和处理器为了提高程序的运行性能,会对指令进行重新排序。即代码会被编译成操作指令,而指令执行顺序可能会发生变化。

    java中创建对象分为 三个步骤【可以简单理解为三条指令】

    1. 分配内存,内存空间初始化
    2. 对象初始化,类的元数据信息,hashCode等信息
    3. 将内存地址返回

    如果2,3顺序发生了变化,另一个线程获得锁时恰好还没有完成对象初始化,即instance指向null,就会重复创建对象。

    静态内部类

    在静态内部类中持有一个对象。

    /**
     * 使用静态内部类实现单例模式
     *
     * @author Jann Lee
     * @date 2019-07-21 14:47
     **/
    public class Singleton {
    
        private Singleton (){}
    
        public static Singleton getInstance() {
            return ClassHolder.singleton;
        }
    
        /**
         * Singleton装载完成后,不会创建对象
         * 调用getInstance时候,静态内部类ClassHolder才进行装载
         */
        private static class ClassHolder {
           private static final Singleton singleton = new Singleton();
        }
    }
    

    静态内部类的实现则是根据java语言特性实现的,即让静态内部类持有一个对象;根据类加载机机制的特点,每个类只会加载一次,并且只有调用getInstance方法时才会加载内部类。这样就能保证对象只被创建一次,无论是否在多线程环境中。

    只包含单个元素的枚举类型

    在java中枚举也是类的一种,实际上枚举的每一个元素都是一个枚举类的对象,可以理解为对类的一种封装,默认私有构造方法,且不能使用public修饰。

    /**
     * 枚举实现单例模式
     *
     * 为了便于理解给枚举类添加了两个属性
     * @author Jann Lee
     * @date 2019-07-21 14:55
     **/
    public enum Singleton {
    
        INSTANCE;
    
        private String name;
    
        private int age;
    
        Singleton04() {
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public int getAge() {
            return age;
        }
    
        public void setAge(int age) {
            this.age = age;
        }
    }
    

    四.要点总结

    • SIngleton模式中的实例构造器可以设置为protected以允许字类派生
    • Singleton模式一般不要支持拷贝构造函数和clone接口,因为这有可能导致多个对象实例,与Singleton模式的初衷违背。
    • 如何实现多线程环境下安全的Singleton,注意对双重检查锁的正确实现。

    五.思考

    1. java中创建对象的方式有很多种,其中当然包括反射,反序列化,那么上述各种设计模式还能保证对象只会被创建一次吗?(这个问题会在下一篇 中进行分析)
    2. volatile关键字是一个非常重要的关键字,它有那些功能?
  • 相关阅读:
    NSAttributedString用法
    xib Nib IB 可视化编程详解
    对虚函数的理解
    QSS的关键词
    Qt发布问题
    Qt使用与问题处理
    Qt术语
    Qt国际化——使用自带翻译包
    Qt 小记
    查看端口及使用进行
  • 原文地址:https://www.cnblogs.com/liqiangchn/p/11222827.html
Copyright © 2011-2022 走看看