如何正确书写单例模式

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

青铜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.单例模式的其他问题《深入浅出单实例

原文地址:https://www.cnblogs.com/echo1937/p/6247050.html