【JUC学习】单例线程安全

164 阅读8分钟

单例模式如何在多线程下保证线程安全?

一、正常的单例模式

正常我们写单例模式的代码时,会从以下几个方面入手:

  1. 构造函数私有化,避免直接被调用构造方法创建多个实例对象。
private Singleton(){}
  1. 直接在类的内部创建一个唯一的实例对象,用static标识可以让该实例从类加载的时候被创建,此时类加载器就可以保证创建该实例时的线程安全。用final标识避免被修改
priavte static final Singleton INSTANCE = new Singleton();
  1. 提供一个静态方法获取该实例,之所以是静态,是因为非静态方法必须通过一个实例对象来调用,而单例模式禁止外部创建实例,就需要使用类名直接调用静态方法,同时静态方法不能访问非静态变量,所以INSTANCE实例也必须是静态的
public static Singleton getInstance(){
    return INSTANCE
}

总结一下就是:

public class Singleton {
    private Singleton() {
    }

    // 静态内部成员变量
    private static final Singleton INSTANCE = new Singleton();

    public static Singleton getInstance() {
        return INSTANCE;
    }

但是其实这个代码还有一些问题,既然Singleton是是单例模式,那么就要避免Singleton这个单例被破坏,导致外部可以创建其他的实例对象。那么就要从以下几个方面考虑:

  1. 子类继承
  2. 反射
  3. 反序列化 首先,为了避免子类继承从而破坏该单例的内部结构,需要加final关键字; 其次,这种情况下是不能防止反射创建新的实例的; 最后,关于反序列化,需要在实现Serializable接口后,实现readResolve方法来保证反序列化后得到的对象和原先的是同一对象
public final class Singleton implements Serializable {
    private Singleton() {
    }

    private static final Singleton INSTANCE = new Singleton();

    public static Singleton getInstance() {
        return INSTANCE;
    }

    public Object readResolve() {
        return INSTANCE;
    }
}

根据上边的代码可以看出INSTANCE其实在类加载的过程中就已经创建了,属于饿汉式加载,但是如果我始终没有用到这个INSTANCE实例,是不是创建的这个实例就是浪费了内存呢?

二、懒汉式单例模式

根据 中的问题,我们可以使用懒汉式加载的方式来更改代码,懒汉式就是在用到的时候才会去进行实例化,没有用到的时候就一直不进行实例化,简单的修改就是把

priavte static final Singleton INSTANCE = new Singleton();

改为

priavte static Singleton INSTANCE = null;

将getInstance方法改为:

public static Singleton getInstance() {
    if (INSTANCE != null) {
        return INSTANCE;
    }
    INSTANCE = new Singleton();
    return INSTANCE;
}

这样的话,在调用getInstance时,没有就去创建,有的话直接返回,这样就解决了内存浪费的问题。但是,这样是线程不安全的。由于INSTANCE不是在类加载的时候实例化的,而是在调用getInstance的时候进行实例化的,这样就可能会发生线程安全的问题,会有多个线程同时进入到

INSTANCE = new Singleton();

这行代码,就会创建出多个实例。为了解决这个问题,最先想到的就是加锁,直接在getInstance方法上加一把锁:

public static synchronized Singleton getInstance() {
    if (INSTANCE != null) {
        return INSTANCE;
    }
    INSTANCE = new Singleton();
    return INSTANCE;
}

这样就没有了线程不安全的问题,但是又会导致效率比较低,因为每次调用getInstance方法都会加一次锁,还是用类来加的锁,实际上加锁只需要在创建实例的时候就行了,实例创建出来之后就没必要再加锁了。

三、单例模式-双重验证锁

根据上边所说的,我们可以将getInstance改造为:

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

这样看起来既解决了加锁时的效率,又保证了线程安全。但是,这样写的代码依旧是线程不安全的

假如有两个线程A和B同时调用了getInstance方法,A方法先获取到了锁进入 new Singleton()这一步,而此时B要获取锁的时候发现锁被A拿了,就会等待A释放锁,A执行完实例化的过程之后释放了锁,此时INSTANCE已经被实例化了,而B拿到了A释放的锁,会直接再进行一次INSTANCE的实例化,这样其实就又发生了线程安全的问题:

image.png 解决这个问题也很简单,在临界区也判断一次INSTANCE是否已经被实例化就可以了:

public static Singleton getInstance() {
    if (INSTANCE != null) {
        return INSTANCE;
    }
    synchronized(Singleton.class){
        // 线程B拿到锁进来之后会再判断一次INSTANCE是否实例化
        if (INSTANCE != null) {
            return INSTANCE;
        }
        INSTANCE = new Singleton();
        return INSTANCE;
    }
}

其中我们两次判断INSTANCE是否已经实例化,所以这种加锁的方式叫做双重验证锁

四、扩展1:单例模式-Lazy initialization holder class模式

综合以上三种单例的实现方式,会发现其实我们一直就在解决两个问题:

  1. 实例初始化时候的线程安全问题;
  2. 避免实例在没有用到的时候就被初始化的资源浪费问题(饿汉式); 第一个单例模式用类加载的方式将线程安全交给JVM实现,但是实例会在类加载的时候就会进行初始化,造成资源浪费,第二、三中实现的是懒汉式加载,但是需要我们自己去实现线程安全。那么有没有一种既可以把线程安全交给JVM来做,又能实现懒汉式的加载方式呢?

Lazy initialization holder class模式就是使用了Java的类级内部类和多线程缺省同步锁的知识,巧妙的实现了懒汉式加载和线程安全。

前置知识:

  1. 类级内部类
    • 就是有static修饰的成员式内部类,如果没有被static修饰就是对象级内部类;
    • 类级内部类相当于其外部类的static成分,它的对象与外部类的对象不存在依赖关系,因此可以直接创建。而对象级内部类的实例,是绑定在外部对象的实例中的;
    • 类级内部类中,可以定义静态方法。在静态方法中只能引用外部类中的静态成员变量方法或成员变量;
    • 类级内部类相当于其外部类的成员,只有在第一次被使用时才会被加载。
  2. 多线程缺省同步锁,一般在多线程的开发中,为了解决线程安全的问题,主要是通过synchronized来加互斥锁进行同步控制。但是在某些情况中,JVM已经隐含的执行了同步,这些情况下就不用自己来进行同步控制了,这些情况包括:
    • 由静态初始化器(在静态字段上或static{}块中的初始化器)初始化数据时
    • 访问final字段时
    • 在创建线程之前创建对象时
    • 线程可以看见它将要处理的对象时 解决的思路: 首先,我们想把线程安全的问题抛给JVM去解决,根据第一个单例模式,我们可以使用静态初始化器的方式:
priavte static final Singleton INSTANCE = new Singleton();

但是这样一来不就又会造成一定的空间浪费吗?所以现在在这个基础上要解决的问题就是让类加载的时候不去初始化INSTANCE这个对象,而用到的方法就是类级内部类的一个特性: 类级内部类相当于其外部类的成员,只有在第一次被使用时才会被加载。 因此我们可以将上边这行代码放到一个类级内部类中去:

private static class LazyHolder {
    static final Singleton3 INSTANCE = new Singleton3();
}

这样在类装载时就不会初始化LazyHolder,自然INSTANCE也不会被初始化,实现了延迟加载,也就是懒汉式。 整体代码:

public class Singleton {
    private Singleton() {
    }

    // 懒汉式
    private static class LazyHolder {
        static final Singleton INSTANCE = new Singleton3();
    }

    // 没有并发问题,因为属于类加载
    public static Singleton getInstance() {
        return LazyHolder.INSTANCE;
    }
}

五、扩展2:单例模式-枚举实现

以上四种单例模式除了延迟加载和线程安全问题外,还有一个重要的问题没有解决:单例对象的唯一性。 之前我们解决了子类继承对单例的破坏,解决了反序列化对单例的破坏,但是唯独没有解决反射对单列的破坏。要解决反射对单例的破坏问题,就要用枚举的方式来实现。

单元素的枚举类型已经成为实现Singleton的最佳方法。

枚举是在JDK1.5之后增加的一个“语法糖”,它主要用于维护一些实例对象固定的类。

因为JVM会保证枚举对象的唯一性,因此每一个枚举类型和定义的枚举变量在JVM中都是唯一的。最简单的实现如下:

public enum Singleton {
    INSTANCE
}

当然这样只是一个实例,没有任何的方法,提供一个简单的业务方法:

public enum Singleton {
     INSTANCE;
     public void businessMethod() {
          System.out.println("我是一个单例!");
     }
}

调用:

public static void main(String[] args) {
    SingletonEnum.INSTANCE.businessMethod(); // "我是一个单例!"
}

如果要将一个已有的类改造为单例类,也可以使用枚举的方式来实现。

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

调用:

    public static void main(String args[]) {
    	Singleton s1 = SingletonEnum.SINGLETON.getInstance();
    	Singleton s2 = SingletonEnum.SINGLETON.getInstance();
    	System.out.println(s1==s2);
    }