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

    单例模式

      单例模式是23中设计模式中比较简单的一种,其核心思想是一个类只有一个实例,该类自己创建这一唯一实例并提供该实例的全局访问方法

    单例模式的应用场景

    在说单例模式之前,来想象几个场景:

    在我们的windows桌面上,我们打开了一个回收站,当我们试图再次打开一个新的回收站时,Windows系统并不会为你弹出一个新的回收站窗口。,也就是说在整个系统运行的过程中,系统只维护一个回收站的实例。这就是一个典型的单例模式运用。(任务管理器也是一样哦)

    我们在实际使用中并不存在需要同时打开两个回收站窗口的必要性。假如我每次创建回收站时都需要消耗大量的资源,而每个回收站之间资源是共享的,那么在没有必要多次重复创建该实例的情况下,创建了多个实例,这样做就会给系统造成不必要的负担,造成资源浪费。(数据库连接池和线程池也比较类似)

    网站计数器,如果有多个网站计数器,会造成数据难以同步,因此也采用单例模式(应用程序的日志应用,一般也采用单例,这样可以很方便地进行数据追加)
    参考

    通过这几个应用场景不难看出单例模式的合适应用范围:

    1. 需要生成唯一序列的环境
    2. 需要频繁实例化然后销毁的对象。
    3. 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
    4. 方便资源相互通信的环境

    饿汉式单例

    public class EagerSingleton {
        private static EagerSingleton instance = new EagerSingleton();
        /**
         * 私有默认构造方法
         */
        private EagerSingleton(){}
        /**
         * 静态工厂方法
         */
        public static EagerSingleton getInstance(){
            return instance;
        }
    }
    

      在上例中,在这个类被加载时,静态变量instance会被初始化,此时类的私有构造方法就会被调用。这是一种典型的空间换时间,管你需不需要,先创建出来再说,调用的时候就可以了直接调用,节省了时间。

    懒汉式单例

    public class LazySingleton {
        private static LazySingleton instance = null;
        /**
         * 私有默认构造方法
         */
        private LazySingleton(){}
        /**
         * 静态工厂方法
         */
        public static synchronized LazySingleton getInstance(){
            if(instance == null){
                instance = new LazySingleton();
            }
            return instance;
        }
    }
    

      上面的懒汉式单例类实现里对静态工厂方法使用了同步化,以处理多线程环境。懒汉式是典型的时间换空间,就是每次获取实例都会进行判断,看是否需要创建实例,浪费判断的时间。当然,如果一直没有人使用的话,那就不会创建实例,则节约内存空间。
      这种方式虽然线程安全,但是每次调用都会加锁开锁,效率较低因此可是继续优化。

    双重检查锁

      所谓“双重检查加锁”机制,指的是:并不是每次进入getInstance方法都需要同步,而是先不同步,进入方法后,先检查实例是否存在,如果不存在才进行下面的同步块,这是第一重检查,进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。
      “双重检查加锁”机制的实现会使用关键字volatile的原因:
    首先需要知道volatile关键字的作用:

    1. 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的;
    2. 禁止指令重排序,保证代码的有序性;

    在并发编程中有三个重要的概念:原子性,可见性,有序性。
    (参考此博文)
    原子性
    原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

      一个很经典的例子就是银行账户转账问题:
      比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。
      试想一下,如果这2个操作不具备原子性,会造成什么样的后果。假如从账户A减去1000元之后,操作突然中止。然后又从B取出了500元,取出500元之后,再执行 往账户B加上1000元 的操作。这样就会导致账户A虽然减去了1000元,但是账户B没有收到这个转过来的1000元。
      所以这2个操作必须要具备原子性才能保证不出现一些意外的问题。
      同样地反映到并发编程中会出现什么结果呢?
      举个最简单的例子,大家想一下假如为一个32位的变量赋值过程不具备原子性的话,会发生什么后果?
    i = 9;
      假若一个线程执行到这个语句时,我暂且假设为一个32位的变量赋值包括两个过程:为低16位赋值,为高16位赋值。
      那么就可能发生一种情况:当将低16位数值写入之后,突然被中断,而此时又有一个线程去读取i的值,那么读取到的就是错误的数据。

    可见性
      可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
      举个简单的例子,看下面这段代码:

    //线程1执行的代码
    int i = 0;
    i = 10;
     
    //线程2执行的代码
    j = i;
    

      假若执行线程1的是CPU1,执行线程2的是CPU2。由上面的分析可知,当线程1执行 i =10这句时,会先把i的初始值加载到CPU1的高速缓存中,然后赋值为10,那么在CPU1的高速缓存当中i的值变为10了,却没有立即写入到主存当中。
      此时线程2执行 j = i,它会先去主存读取i的值并加载到CPU2的缓存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10。
      这就是可见性问题,线程1对变量i修改了之后,线程2没有立即看到线程1修改的值。

    有序性
    有序性:即程序执行的顺序按照代码的先后顺序执行。举个简单的例子,看下面这段代码:

    int i = 0;              
    boolean flag = false;
    i = 1;                //语句1  
    flag = true;          //语句2
    

      上面代码定义了一个int型变量,定义了一个boolean类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句1是在语句2前面的,那么JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗?不一定,为什么呢?这里可能会发生指令重排序(Instruction Reorder)。
      下面解释一下什么是指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
      比如上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。
      但是要注意,虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同,那么它靠什么保证的呢?再看下面一个例子:

    int a = 10;    //语句1
    int r = 2;    //语句2
    a = a + 3;    //语句3
    r = a*a;     //语句4
    

      这段代码有4个语句,那么可能的一个执行顺序是:语句2 -> 语句1 -> 语句3 -> 语句4。
      那么可不可能是这个执行顺序呢: 语句2 -> 语句1 -> 语句4 -> 语句3 呢?
      不可能,因为处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行。
      虽然重排序不会影响单个线程内程序执行的结果,但是多线程呢?下面看一个例子:

    //线程1:
    context = loadContext();   //语句1
    inited = true;             //语句2
     
    //线程2:
    while(!inited ){
      sleep()
    }
    doSomethingwithconfig(context);
    

      上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。
      从上面可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。
      也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。


    扯远了,优化的双重检查锁可以这么写:

    public class Singleton {
        //使用volatile修饰
        private volatile static Singleton instance = null;
        private Singleton(){}
        public static Singleton getInstance(){
            //先检查实例是否存在,如果不存在才进入下面的同步块
            if(instance == null){
                //同步块,线程安全的创建实例
                synchronized (Singleton.class) {
                    //再次检查实例是否存在,如果不存在才真正的创建实例
                    if(instance == null){
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
    

      这种实现方式既可以实现线程安全地创建实例,而又不会对性能造成太大的影响。它只是第一次创建实例的时候同步,以后就不需要同步了,从而加快了运行速度。
      由于volatile关键字可能会屏蔽掉虚拟机中一些必要的代码优化,所以运行效率并不是很高。因此一般建议,没有特别的需要,不要使用。也就是说,虽然可以使用“双重检查加锁”机制来实现线程安全的单例,但并不建议大量采用,可以根据情况来选用。
      根据上面的分析,常见的两种单例实现方式都存在小小的缺陷,那么有没有一种方案,既能实现延迟加载,又能实现线程安全呢?

    Lazy initialization holder class模式

      这个模式综合使用了Java的类级内部类和多线程缺省同步锁的知识,很巧妙地同时实现了延迟加载和线程安全。
    相应的基础知识:

    • 什么是类级内部类?
        简单点说,类级内部类指的是,有static修饰的成员式内部类。如果没有static修饰的成员式内部类被称为对象级内部类。
        类级内部类相当于其外部类的static成分,它的对象与外部类对象间不存在依赖关系,因此可直接创建。而对象级内部类的实例,是绑定在外部对象实例中的。
        类级内部类中,可以定义静态的方法。在静态方法中只能够引用外部类中的静态成员方法或者成员变量。
        类级内部类相当于其外部类的成员,只有在第一次被使用的时候才被会装载。

    • 多线程缺省同步锁的知识
        在多线程开发中,为了解决并发问题,主要是通过使用synchronized来加互斥锁进行同步控制。但是在某些情况中,JVM已经隐含地为您执行了同步,这些情况下就不用自己再来进行同步控制了。这些情况包括:

      1. 由静态初始化器(在静态字段上或static{}块中的初始化器)初始化数据时
      2. 访问final字段时
      3. 在创建线程之前创建对象时
      4. 线程可以看见它将要处理的对象时

    解决方案的思路:
      要想很简单地实现线程安全,可以采用静态初始化器的方式,它可以由JVM来保证线程的安全性。比如前面的饿汉式实现方式。但是这样一来,不是会浪费一定的空间吗?因为这种实现方式,会在类装载的时候就初始化对象,不管你需不需要。
      如果现在有一种方法能够让类装载的时候不去初始化对象,那不就解决问题了?一种可行的方式就是采用类级内部类,在这个类级内部类里面去创建对象实例。这样一来,只要不使用到这个类级内部类,那就不会创建对象实例,从而同时实现延迟加载和线程安全。
      示例代码如下:

    public class Singleton {
        private Singleton(){}
        /**
         *    类级的内部类,也就是静态的成员式内部类,该内部类的实例与外部类的实例
         *    没有绑定关系,而且只有被调用到时才会装载,从而实现了延迟加载。
         */
        private static class SingletonHolder{
            /**
             * 静态初始化器,由JVM来保证线程安全
             */
            private static Singleton instance = new Singleton();
        }
        
        public static Singleton getInstance(){
            return SingletonHolder.instance;
        }
    }
    

      当getInstance方法第一次被调用的时候,它第一次读取SingletonHolder.instance,导致SingletonHolder类得到初始化;而这个类在装载并被初始化的时候,会初始化它的静态域,从而创建Singleton的实例,由于是静态的域,因此只会在虚拟机装载类的时候初始化一次,并由虚拟机来保证它的线程安全性。
      这个模式的优势在于,getInstance方法并没有被同步,并且只是执行一个域的访问,因此延迟初始化并没有增加任何访问成本。

    单例和枚举

      按照《高效Java 第二版》中的说法:单元素的枚举类型已经成为实现Singleton的最佳方法。用枚举来实现单例非常简单,只需要编写一个包含单个元素的枚举类型即可。

    public enum Singleton {
        /**
         * 定义一个枚举的元素,它就代表了Singleton的一个实例。
         */
        uniqueInstance;
        
        /**
         * 单例可以有自己的操作
         */
        public void singletonOperation(){
            //功能处理
        }
    }
    

      使用枚举来实现单实例控制会更加简洁,而且无偿地提供了序列化机制,并由JVM从根本上提供保障,绝对防止多次实例化,是更简洁、高效、安全的实现单例的方式。

    小结

    为什么说枚举模式的单例才是最好的呢?

    1. 书写简单,仅仅几行代码就能实现单例模式。
    2. 枚举类能防止利用反射方式获取枚举对象,进而破坏单例模式。
    3. 枚举类能防止使用序列化与反序列化获取新的枚举对象,进而破坏单例模式

    参考链接

  • 相关阅读:
    子程序的设计
    多重循环程序设计
    汇编语言的分支程序设计与循环程序设计
    代码调试之串口调试2
    毕昇杯模块之光照强度传感器
    毕昇杯之温湿度采集模块
    【CSS】盒子模型 之 IE 与W3C的盒子模型对比
    【css】盒子模型 之 概述
    【css】盒子模型 之 弹性盒模型
    【网络】dns_probe_finished_nxdomain 错误
  • 原文地址:https://www.cnblogs.com/lixin-link/p/11077501.html
Copyright © 2011-2022 走看看