zoukankan      html  css  js  c++  java
  • 设计模式:单例模式介绍及8种写法(饿汉式、懒汉式、Double-Check、静态内部类、枚举)


    一、饿汉式(静态常量)


    这种饿汉式的单例模式构造的步骤如下:

    1. 构造器私有化;(防止用new来得到对象实例)
    2. 类的内部创建对象;(因为1,所以2)
    3. 向外暴露一个静态的公共方法;(getInstance)

    示例:

    class Singleton{
        //1私有化构造方法
        private Singleton(){
    
        }
        //2创建对象实例
        private final static Singleton instance = new Singleton();
        //3对外提供公有静态方法
        public static Singleton getInstance(){
            return instance;
        }
    }
    

    这样的话,获取对象就不能通过 new 的方式,而要通过 Singleton.getInstance();并且多次获取到的都是同一个对象。

    使用静态常量的饿汉式写法实现的单例模式的优缺点:

    优点:

    简单,类装载的时候就完成了实例化,避免了多线程同步的问题。

    缺点:

    类装载的时候完成实例化,没有达到 Lazy Loading (懒加载)的效果,如果从始至终都没用过这个实例呢?那就会造成内存的浪费。(大多数的时候,调用getInstance方法然后类装载,是没问题的,但是导致类装载的原因有很多,可能有其他的方式或者静态方法导致类装载)

    总结:

    如果确定会用到他,这种写是没问题的,但是尽量避免内存浪费。

    二、饿汉式(静态代码块)


    和上一种用静态常量的方法类似,是把创建实例的过程放在静态代码块里。

    class Singleton{
        //1同样私有化构造方法
        private Singleton(){
    
        }
        //2创建对象实例
        private static Singleton instance;
        //在静态代码块里进行单例对象的创建
        static {
            instance = new Singleton();
        }
        //3提供静态方法返回实例对象
        public static Singleton getInstance() {
            return instance;
        }
    }
    

    优缺点:和上一种静态常量的方式一样;

    原因:实现本来就是和上面的一样,因为类装载的时候一样马上会执行静态代码块中的代码。

    三、懒汉式(线程不安全)


    上面的两种饿汉式,都是一开始类加载的时候就创建了实例,可能会造成内存浪费。

    懒汉式的写法如下:

    class Singleton{
        private static Singleton instance;
        private Singleton(){
    
        }
        //提供静态公有方法,使用的时候才创建instance
        public static Singleton getInstance(){
            if(instance == null){
                instance = new Singleton();
            }
            return  instance;
        }
    }
    

    也就是说,同样是 1) 私有构造器;2) 类的内部创建实例;3) 向外暴露获取实例方法。这三个步骤。

    但是懒汉式的写法,将创建的代码放在了 getInstance 里,并且只有第一次的时候会创建,这样的话,类加载的过程就不会创建实例,同时也保证了创建只会有一次。

    优点:

    起到了Lazy Loading 的作用

    缺点:

    但是只能在单线程下使用。如果一个线程进入了 if 判断,但是没来得及向下执行的时候,另一个线程也通过了这个 if 语句,这时候就会产生多个实例,所以多线程环境下不能使用这种方式。

    结论:

    实际开发不要用这种方式。

    四、懒汉式(线程安全,同步方法)


    因为上面说了主要的问题,就在于 if 的执行可能不同步,所以解决的方式也很简单。

    class Singleton{
        private static Singleton instance;
        private Singleton(){
    
        }
        //使用的时候才创建instance,同时加入synchronized同步代码,解决线程不安全问题
        public static synchronized Singleton getInstance(){
            if(instance == null){
                instance = new Singleton();
            }
            return  instance;
        }
    }
    

    只要在获取实例的静态方法上加上 synchronized 关键字,同步机制放在getInstance方法层面,就 ok。

    优点:

    保留了单例的性质的情况下,解决了线程不安全的问题

    缺点:

    效率太差了,每个线程想要获得类的实例的时候都调用 getInstance 方法,就要进行同步。
    然而这个方法本身执行一次实例化代码就够了,后面的想要获得实例,就应该直接 return ,而不是进行同步。

    结论:

    实际开发仍然不推荐

    五、懒汉式(同步代码块)


    这种写法是基于对上一种的思考,既然在方法层面效率太差,那直接在实例化的语句上加 synchronized 来让他同步,是不是就能解决效率问题呢?

    class Singleton{
        private static Singleton instance;
        private Singleton(){
    
        }
        
        public static Singleton getInstance(){
            if(instance == null){
                synchronized( Singleton.class){
                    instance = new Singleton();
                }
            }
            return  instance;
        }
    }
    

    事实上,这种方法,让 synchronized 关键字放入方法体里,又会导致可能别的线程同样进入 if 语句,回到了第三种的问题,所以来不及同步就会产生线程不安全的问题。

    结论:不可用

    六、 双重检查Double Checked Locking


    在下面的实例化过程里采用 double check locking,也就是两次判断。

    class Singleton{
        private static Singleton instance;
        private Singleton(){
    
        }
        //双重检查
        public static Singleton getInstance(){
            //第一次检查
            if(instance == null){
                synchronized (Singleton.class){
                    //第二次检查
                    if(instance == null){
                        instance = new Singleton();
                    }
                }
            }
            return  instance;
        }
    }
    

    4 的懒汉式同步方法写法里,getInstance方法是用了synchronized修饰符,所以虽然解决了 lazy loading 的问题,线程也安全,但是同步起来会很慢。

    而 5 的懒汉式同步代码块写法,将 synchronized 修饰符加到内部的代码块部分,又会导致线程安全直接失效,因为可能大家都同时进入了 getInstance 方法。

    所以double - checked - locking 将两者相结合。

    可是这样的 double - checked - locking 还不能保证线程安全,原因涉及到多线程里的知识点,指令重排序:(待更新链接)

    看起来只有一句的 instance = new Singleton(); 实际上是分为三个步骤的:

    1). 申请一块内存空间,用来装 new 出来的对象;
    2). 初始化对象信息;
    3). 返回对象地址,建立连接。

    而这些指令可能由于重排,先执行了第 3 步,而第 2 步由于耗时过多还在进行,此时另一个线程执行,虽然判断出了 instance 不为空,可是他直接返回的对象是return instance,这块地址的内容是不对的,可能是空指针,可能是错误的数据。

    解决这个终极问题的方式是,使用 volatile 关键字,让修改值立即更新到主存。

    class Singleton{
        private static volatile Singleton instance;
        private Singleton(){
    
        }
        //双重检查
        public static Singleton getInstance(){
            //第一次检查
            if(instance == null){
                synchronized (Singleton.class){
                    //第二次检查
                    if(instance == null){
                        instance = new Singleton();
                    }
                }
            }
            return  instance;
        }
    }
    

    优点

    double-check是多线程开发里经常用到的,满足了我们需要的线程安全&&避免反复进行同步的效率差&&lazy loading。

    结论:推荐使用。

    七、静态内部类


    静态内部类:用static修饰的内部类,称为静态内部类,完全属于外部类本身,不属于外部类某一个对象,外部类不可以定义为静态类,Java中静态类只有一种,那就是静态内部类。

    class Singleton{
        //构造器私有化
        private Singleton(){
    
        }
        //一个静态内部类,里面有一个静态属性,就是实例
        private static class SingletonInstance{
            private static final Singleton instance = new Singleton();
        }
        //静态的公有方法
        public static Singleton getInstance(){
            return SingletonInstance.instance;
        }
    }
    

    核心:

    1. 静态内部类在外部类装载的时候并不会执行,也就是满足了 lazy loading;
    2. 调用getInstance的时候会取属性,此时才加载静态内部类,而 jvm 底层的类装载机制是线程安全的,所以利用 jvm 达到了我们要的线程安全;
    3. 类的静态属性保证了实例化也只会进行一次,满足单例。

    结论:推荐。

    八、枚举


    将单例的类写成枚举类型,直接只有一个Instance变量。

    enum Singleton{
        instance;
        public void sayOk(){
            System.out.println("ok");
        }
    }
    

    调用的时候也不用new,直接用Singleton.instance,拿到这个属性。(一般INSTANCE写成大写)

    优点:

    满足单例模式要的特点,同时还能够避免反序列化重新创建新的对象。
    这种方法是effective java作者提供的方式。

    结论:推荐。

    九、总结


    单例模式使用的场景是

    需要频繁创建和销毁的对象、创建对象耗时过多或耗资源太多(重型对象)、工具类对象、频繁访问数据库或者文件的对象(数据源、session工厂等),都应用单例模式去实现。

    因为单例模式保证了系统内存中只存在该类的一个对象,所以能节省资源,提高性能,那么对外来说,单例的类都不能再通过 new 去创建了,而是采用类提供的获取实例的方法。

    上面的八种写法里面:饿汉式两种基本是一样的写法,懒汉式三种都有问题,以上物种的改进就是双重检查,另辟蹊径的是静态内部类和枚举。

    所以,单例模式推荐的方式有四种:

    1. 饿汉式可用(虽然内存可能会浪费);
    2. 双重检查;
    3. 静态内部类;
    4. 枚举。

    十、单例模式在JDK里的应用


    Runtime类就是一个单例模式的类,并且可以看到,他是采用我们所说的第一种方式,即饿汉式(静态常量的方式)

    1. 私有构造器;
    2. 静态常量,类的内部直接将类实例化;
    3. 提供公有的静态方法。
  • 相关阅读:
    CIFAR10-网络训练技术
    论文-Deep Residual Learning for Image Recognition
    Origin-作图相关
    OnCtlColor
    查看电脑MAC地址
    改变静态文本框和PictureControl的背景颜色
    MFC在对话框中的Picture contrl控件中添加icon图标,并改变icon图标的背景色与对话框背景色一致
    char型数组学习
    条件编译
    ASCII码
  • 原文地址:https://www.cnblogs.com/lifegoeson/p/13474269.html
Copyright © 2011-2022 走看看