关于设计模式:设计模式1-单例模式到底几种写法

40次阅读

共计 4712 个字符,预计需要花费 12 分钟才能阅读完成。

[TOC]

单例模式,是一种比较简单的设计模式,也是属于创立型模式(提供一种创建对象的模式或者形式)。
要点:

    • 1. 波及一个繁多的类,这个类来创立本人的对象(不能在其余中央重写创立办法,初始化类的时候创立或者提供公有的办法进行拜访或者创立,必须确保只有单个的对象被创立)。
    • 2. 单例模式不肯定是线程不平安的。
    • 3. 单例模式能够分为两种:懒汉模式 (在第一次应用类的时候才创立,能够了解为类加载的时候特地懒,要用的时候才去获取,要是没有就创立,因为是单例,所以只有第一次应用的时候没有,创立后就能够始终用同一个对象), 饿汉模式(在类加载的时候就曾经创立,能够了解为饿汉曾经饿得饥渴难耐,必定先把资源紧紧拽在本人手中,所以在类加载的时候就会先创立实例)

      关键字:

      • 单例:singleton
      • 实例:instance
      • 同步:synchronized

    饿汉模式

    1. 公有属性

    第一种 singlepublic,能够间接通过 Singleton 类名来拜访。

     public class Singleton {
        // 私有化构造方法,以避免外界应用该构造方法创立新的实例
        private Singleton(){}
        // 默认是 public,拜访能够间接通过 Singleton.instance 来拜访
        static Singleton instance = new Singleton();}

    2. 私有属性

    第二种是用 private 润饰singleton,那么就须要提供static 办法来拜访。

    public class Singleton {private Singleton(){ }
        // 应用 private 润饰,那么就须要提供 get 办法供外界拜访
        private static Singleton instance = new Singleton();
        // static 将办法归类所有,间接通过类名来拜访
        public static Singleton getInstance(){return instance;.}
    }

    3. 懒加载

    饿汉模式,这样的写法是没有问题的,不会有线程平安问题(类的 static 成员创立的时候默认是上锁的,不会同时被多个线程获取到),然而是有毛病的,因为 instance 的初始化是在类加载的时候就在进行的,所以类加载是由 ClassLoader 来实现的,那么初始化得比拟早益处是起初间接能够用,害处也就是节约了资源,要是只是个别类应用这样的办法,依赖的数据量比拟少,那么这样的办法也是一种比拟好的单例办法。
    在单例模式中个别是调用 getInstance() 办法来触发类装载,以上的两种饿汉模式显然没有实现 lazyload(集体了解是用的时候才触发类加载)
    所以上面有一种饿汉模式的改进版,利用外部类实现懒加载。
    这种形式Singleton 类 被加载了,然而 instance 也不肯定被初始化,要等到 SingletonHolder 被被动应用的时候,也就是显式调用 getInstance() 办法的时候,才会显式的装载 SingletonHolder 类,从而实例化instance。这种办法应用类装载器保障了只有一个线程可能初始化instance,那么也就保障了单例,并且实现了懒加载。

    值得注意的是:动态外部类尽管保障了单例在多线程并发下的线程安全性,然而在遇到序列化对象时,默认的形式运行失去的后果就是多例的。

    public class Singleton {private Singleton(){ }
        // 外部类
        private static class SingletonHolder{private static final Singleton instance = new Singleton();
        }
        // 对外提供的不容许重写的获取办法
        public static final Singleton getInstance(){return SingletonHolder.instance;}
    }

    懒汉模式

    最根底的代码(线程不平安)

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

    这种写法,是在每次获取实例 instance 的时候进行判断,如果没有那么就会 new 一个进去,否则就间接返回之前曾经存在的 instance。然而这样的写法 不是线程平安的 ,当有多个线程都执行getInstance() 办法的时候,都判断是否等于 null 的时候,就会各自创立新的实例,这样就不能保障单例了。所以咱们就会想到同步锁,应用 synchronized 关键字:
    加同步锁的代码(线程平安,效率不高)

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

    这样的话,getInstance()办法就会被锁上,当有两个线程同时拜访这个办法的时候,总会有一个线程先取得了同步锁,那么这个线程就能够执行上来,而另一个线程就必须期待,期待第一个线程执行完 getInstance() 办法之后,才能够执行。这段代码 是线程平安的 ,然而效率不高,因为如果有很多线程,那么就必须让所有的都期待正在拜访的线程,这样就会 大大降低了效率。那么咱们有一种思路就是,将锁呈现期待的概率再升高,也就是咱们所说的双重校验锁(双检锁)。

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

    1.第一个 if 判断,是为了升高锁的呈现概率,前一段代码,只有执行到同一个办法都会触发锁,而这里只有 singleton 为空的时候才会触发,第一个进入的线程会创建对象,等其余线程再进入时对象已创立就不会持续创立,如果对整个办法同步,所有获取单例的线程都要排队,效率就会升高。
    2.第二个 if 判断是和之前的代码起一样的作用。

    下面的代码看起来曾经像是没有问题了,事实上,还有有很小的概率呈现问题,那么咱们先来理解:原子操作 指令重排

    1. 原子操作

    • 原子操作,能够了解为不可分割的操作,就是它曾经小到不能够再切分为多个操作进行,那么在计算机中要么它齐全执行了,要么它齐全没有执行,它不会存在执行到中间状态,能够了解为没有中间状态。比方:赋值语句就是一个原子操作:
     n = 1; // 这是一个原子操作 

    假如 n 的值以前是 0,那么这个操作的背地就是要么执行胜利 n 等于 1,要么没有执行胜利 n 等于 0,不会存在中间状态,就算是并发的过程中也是一样的。
    上面看一句 不是原子操作 的代码:

    int n =1;  // 不是原子操作

    起因:这个语句中能够拆分为两个操作,1. 申明变量 n,2. 给变量赋值为 1,从中咱们能够看出有一种状态是 n 被申明后然而没有来得及赋值的状态,这样的状况,在并发中,如果多个线程同时应用 n,那么就会可能导致不稳固的后果。

    2. 指令重排

    所谓指令重排,就是计算机会对咱们代码进行优化,优化的过程中会在不影响最初后果的前提下,调整原子操作的程序。比方上面的代码:

    int a ;   // 语句 1 
    a = 1 ;   // 语句 2
    int b = 2 ;     // 语句 3
    int c = a + b ; // 语句 4 

    失常的状况,执行程序应该是 1234,然而理论有可能是 3124,或者 1324,这是因为语句 3 和 4 都没有原子性问题,那么就有可能被拆分成原子操作,而后重排.
    原子操作以及指令重排的根本理解到这里完结,看回咱们的代码:

    次要是instance = new Singleton(),依据咱们所说的,这个语句不是原子操作,那么就会被拆分,事实上 JVM(java 虚拟机)对这个语句做的操作:

    • 1. 给 instance 调配了内存
    • 2. 调用 Singleton 的构造函数初始化了一个成员变量,产生了实例,放在另一处内存空间中
    • 3. 将 instance 对象指向调配的内存空间,执行完这一步才算真的实现了,instance 才不是 null。

    在一个线程外面是没有问题的,那么在多个线程中,JVM 做了指令重排的优化就有可能导致问题,因为第二步和第三步的程序是不可能保障的,最终的执行程序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行结束、2 未执行之前,被线程二抢占了,这时 instance 曾经是 非 null 了(但却没有初始化),所以线程二会间接返回 instance,而后应用,就会报空指针。
    从更上一层来说,有一个线程是 instance 曾经不为 null 然而仍没有实现初始化 中间状态,这个时候有一个线程刚刚好执行到第一个 if(instance==null), 这里失去的 instance 曾经不是 null,而后他间接拿来用了,就会呈现谬误。
    对于这个问题,咱们应用的计划是加上 volatile 关键字。

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

    volatile的作用:禁止指令重排,把 instance 申明为 volatile 之后,这样,在它的赋值实现之前,就不会调用读操作。也就是在一个线程没有彻底实现instance = new Singleton(); 之前,其余线程不可能去调用读操作。

    • 下面的办法实现单例都是基于没有 简单序列化和反射 的时候,否则还是有可能有问题的,还有最初一种办法是应用枚举来实现单例,这个能够说的比拟理想化的单例模式,主动反对序列化机制,相对避免屡次实例化。
    public enum Singleton {
        INSTANCE;
        public void doSomething() {}
    }

    以上最举荐枚举形式,当然当初计算机的资源还是比拟足够的,饿汉形式也是不错的,其中懒汉模式下,如果波及多线程的问题,也须要留神写法。

    最初揭示一下,volatile关键字,只禁止指令重排序,保障可见性(一个线程批改了变量,对任何其余线程来说都是立刻可见的,因为会立刻同步到主内存),然而不保障原子性。

    【作者简介】
    秦怀,公众号【秦怀杂货店】作者,技术之路不在一时,山高水长,纵使迟缓,驰而不息。这个世界心愿所有都很快,更快,然而我心愿本人能走好每一步,写好每一篇文章,期待和你们一起交换。

    此文章仅代表本人(本菜鸟)学习积攒记录,或者学习笔记,如有侵权,请分割作者核实删除。人无完人,文章也一样,文笔稚嫩,在下不才,勿喷,如果有谬误之处,还望指出,感激不尽~

    正文完
     0