zoukankan      html  css  js  c++  java
  • 如何正确书写单例模式

    一般单例都是五种写法:懒汉、饿汉、双重校验锁、静态内部类和枚举。为了记录学习的过程,这里整理了几种常见的单例写法。

    青铜5:(Lazy-loaded,但线程不安全)

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

    这段代码简单明了,而且实现了延迟加载,但是在多线程环境下,如果多个线程同时调用 getInstance() 方法,可能会发生多个线程同时通过(instance==null)的条件检测,从而进入if语句的程序代码块创建出多个实例。

    懒汉式:synchronized(Lazy-loaded,线程安全,但不高效)

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

    为了解决上面的问题,最简单的方法是将整个 getInstance() 方法设为同步(synchronized),使得只要是进入getInstance() 方法的线程都得同步。实际上,创建实例对象的动作只有一次,后面的方法调用都是读取成员变量,对后续的读操作也进行线程同步会严重影响性能。

    饿汉式:static final field(非Lazy-loaded)

    这种方法非常简单,因为单例的实例被声明成 static final,在第一次加载类到内存中时就会初始化,创建实例对象也是线程安全的(由JVM实现保证)。

    public class Singleton{
        //类加载时就初始化
        private static final Singleton instance = new Singleton();
        private Singleton(){}
        
        public static Singleton getInstance(){                       // Singleton with static factory
            return instance;
        }
    }

    它没有实现延迟加载,即使没有调用getInstance()方法,instance也会在类加载后就被初始化。这会导致一些使用限制:譬如 Singleton 实例的创建依赖配置文件或者参数,必须在执行 getInstance() 之前调用某个方法设置参数给它,那样这种单例写法就无法使用了。类似的方法还有:

    public class Singleton{
        public static final Singleton instance = new Singleton();    // Singleton with public final field
        private Singleton(){}
        
    }
    // <Effective Java>第14页讲诉了两者的差异

    双重检验锁 + volatile(Lazyload,线程安全,但晦涩)

    所谓“双重检查加锁”指的是:

    并不是每次进入getInstance()方法都需要同步,而是先不同步,进入方法后,先检查实例是否存在,如果不存在才进行下面的同步块,这是第一重检查;进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来就只需要同步一次,从而减少了多次在同步情况下进行判断所浪费的时间。

    public static Singleton getSingleton() {
        if (instance == null) {                         //Single Checked
            synchronized (Singleton.class) {
                if (instance == null) {                 //Double Checked
                    instance = new Singleton();
                }
            }
        }
        return instance ;
    }

    这段代码看起来很完美,很可惜它是有问题的。主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情:

    memory = allocate();   //1:分配对象的内存空间
    ctorInstance(memory);  //2:初始化对象
    instance = memory;     //3:设置instance指向刚分配的内存地址

    但是在 JVM 的JIT编译器中存在指令重排序的优化。也就是说上面的第2步和第3步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。为此,我们需要将 instance 变量声明成 volatile 。

    public class Singleton {
        private volatile static Singleton instance; //声明为 volatile
        private Singleton(){}
    
        public static Singleton getSingleton() {
            if (instance == null) {                         
                synchronized (Singleton.class) {
                    if (instance == null) {       
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }  
    }

    但是特别注意在 Java 1.5 以前的版本使用了 volatile 的双检锁还是有问题的,这个问题在 Java 1.5 中才得以修复,所以在这之后才可以放心使用 volatile。

    静态内部类:IoDH,initialization-on-demand holder 

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

    public class Singleton {
        private Singleton() {}
    
        private static class LazyHolder {
            private static final Singleton INSTANCE = new Singleton();
        }
    
        public static Singleton getInstance() {           // From wikipedia
            return LazyHolder.INSTANCE;
        }
    }

    静态内部类相当于其外部类的static部分,只有在第一次被使用的时候才会被装载。它的对象不依赖于外部类对象(实例),因此可以直接创建。

    多线程缺省同步锁

      大家都知道,在多线程开发中,为了解决并发问题,主要是通过使用synchronized来加互斥锁进行同步控制。但是在某些情况中,JVM已经隐含地为您执行了同步,这些情况下就不需要手动进行同步控制了。这些情况包括:

      1.由静态初始化器(在静态字段上或static{}块中的初始化器)初始化数据时

      2.访问final字段时

      3.在创建线程之前创建对象时

      4.线程可以看见它将要处理的对象时

    枚举 Enum

    从Java 1.5起,只需编写一个包含单个元素的枚举类型:

    public enum Singleton {
        INSTANCE;
    }

    这种方法在功能上与公有域方法相近,但是它更加简洁,无偿地提供了序列化机制,绝对防止多次实例化,即使是在面向复杂的序列化或者反射攻击的时候。虽然这种方法还没有广泛采用,但是单元素的枚举类型以及成为实现Singleton的最佳方法。

    解决方案的思路:

    1. 类级(静态)内部类相当于其外部类的成员,只有在第一次被使用的时候才被会装载。

    --------------------以下是几个存疑的细节--------------------

    1.static final到底有哪些细节

    2.static field处的赋值初始化到底和static代码块有先后吗?

    3.P50 in Java EE设计模式解析与应用中的例子真的有Lazyload效果吗?

    4.单例模式的其他问题《深入浅出单实例

  • 相关阅读:
    vue 定义全局函数和变量
    大学感受
    NOIP2018 游记
    NOI2018 游记
    THUSC 2018 游记
    APIO2018 游记
    SXOI2018游记
    poorpool 的 考场 NOI Linux 配置
    关于 poorpool
    NOIP2017 游记
  • 原文地址:https://www.cnblogs.com/echo1937/p/6247050.html
Copyright © 2011-2022 走看看