单例与线程安全

"单例也会暗含线程隐患?"

Posted by Ariescat on June 30, 2020

单例与线程安全

大家先来回顾一下【双重校验单例模式】,单例实例在第一次使用时进行创建:

public class SingletonExample {

    // 私有的默认构造方法,避免外部通过new创建对象。
    private SingletonExample() {
    }

    // 定义单例对象,至少保证有一个对象被创建的。
    private static SingletonExample singletonExample = null;

    // 静态工厂方法
    public static SingletonExample getInstance() {
        // 双重检测机制
        if (singletonExample == null) {
            // 同步锁,判断对象不为空以后,锁着SingletonExample类
            // synchronized修饰的内部,同一时间只能由一个线程可以访问的。
            synchronized (SingletonExample.class) {
                // 再次进行判断,如果singletonExample为空,就进行创建对象。
                if (singletonExample == null) {
                    singletonExample = new SingletonExample();
                }
            }
        }
        return singletonExample;
    }
}

此实现是,线程不安全的,我们来回顾一下创建对象的过程:

1、memory = allocate() 分配对象的内存空间

2、ctorInstance() 初始化对象

3、instance = memory 设置instance指向刚分配的内存

而实际过程中JVM和CPU优化,会发生指令重排:

1、memory = allocate() 分配对象的内存空间

3、instance = memory 设置instance指向刚分配的内存

2、ctorInstance() 初始化对象

所以当A线程执行完memory = allocate()时,singletonExample就不为null了,B线程就会拿着singletonExample作后续操作,而此时singletonExample是还没有执行初始化的!!

解决办法:

将对象的引用保存到volatile类型域或者AtomicReference对象中。

public class SingletonExample {
    // 1、memory = allocate() 分配对象的内存空间
    // 2、ctorInstance() 初始化对象
    // 3、instance = memory 设置instance指向刚分配的内存

    // 私有的默认构造方法,避免外部通过new创建对象。
    private SingletonExample() {
    }

    // 定义单例对象,至少保证有一个对象被创建的。
    // 单例对象 volatile + 双重检测机制 -> 禁止指令重排
    // volatile适用场景做状态标识量、双重检测,此处就是volatile的双重检测使用场景。
    private volatile static SingletonExample singletonExample = null;

    // 静态工厂方法
    public static SingletonExample getInstance() {
        // 双重检测机制
        if (singletonExample == null) {
            // 同步锁,判断对象不为空以后,锁着SingletonExample类
            // synchronized修饰的内部,同一时间只能由一个线程可以访问的。
            synchronized (SingletonExample.class) {
                // 再次进行判断,如果singletonExample为空,就进行创建对象。
                if (singletonExample == null) {
                    singletonExample = new SingletonExample();
                }
            }
        }
        return singletonExample;
    }
}

安全发布对象

1、安全发布对象的发布与逃逸。

​ 发布对象,使一个对象能够被当前范围之外的代码所使用。

​ 对象逸出,一种错误的发布,当一个对象还没有构造完成时,就使它被其他线程所见

​ 如果不正确的发布了可变对象,会造成两种错误,首先是发布线程以外的任何线程都可以看到被发布对象的过期的值。其次呢,线程看到的被发布对象的引用是最新的,然而呢,被发布对象的状态却是过期的,如果一个对象是可变对象,那么它就要被安全发布才可以。

2、安全发布对象的四种方式。

​ 第一种,在静态初始化函数中初始化一个对象引用。

​ 第二种,将对象的引用保存到volatile类型域或者AtomicReference对象中。

​ 第三种,将对象的引用保存到某个正确构造对象的final类型域中。

​ 第四种,将对象的引用保存到一个由锁保护的域中。

其他几种线程安全的单例模式

1、饿汉模式,单例实例在类装载时进行创建

线程安全,第一种,在静态初始化函数中初始化一个对象引用

如果单例类构造方法中没有过多的操作处理,是可以接受的

缺点:如果单例类构造方法中存在过多的操作处理,会导致该类加载的过慢。可能会引起性能问题。

public class SingletonExample {

    // 私有的默认构造方法,避免外部通过new创建对象。
    // 饿汉模式,私有构造方法没有过多处理。饿汉模式创建的对象肯定会在实际中被使用,不会造成资源浪费。
    private SingletonExample() {
    }

    // 定义单例对象,至少保证有一个对象被创建的。在类装载的时候进行创建保证了线程的安全性。
    private static SingletonExample singletonExample = new SingletonExample();

    // 静态工厂方法
    public static SingletonExample getInstance() {
        return singletonExample;
    }
}

2、懒汉模式 ,单例实例在第一次使用时进行创建

线程安全,第四种,将对象的引用保存到一个由锁保护的域中

缺点:方法加synchronized修饰,不推荐,虽然保证了线程安全性,但是带来了性能方面的开销。

public class SingletonExample {

    // 私有的默认构造方法,避免外部通过new创建对象。
    private SingletonExample() {
    }

    // 定义单例对象,至少保证有一个对象被创建的。
    private static SingletonExample singletonExample = null;

    // 静态工厂方法
    // 使用synchronized修饰,方法内部所有实现同一时间内只能由一个线程访问。
    // 因此可以保证线程安全的。
    public static synchronized SingletonExample getInstance() {
        if (null == singletonExample) {
            singletonExample = new SingletonExample();
        }
        return singletonExample;
    }
}

3、饿汉模式 ,单例实例在类装载时进行创建,是线程安全的。

public class SingletonExample {

    // 私有的默认构造方法,避免外部通过new创建对象。
    // 饿汉模式,私有构造方法没有过多处理。饿汉模式创建的对象肯定会在实际中被使用,不会造成资源浪费。
    private SingletonExample() {
    }

    // 定义单例对象,至少保证有一个对象被创建的。在类装载的时候进行创建保证了线程的安全性。
    private static SingletonExample singletonExample = null;

    // 静态块初始化对象singletonExample
    static {
        singletonExample = new SingletonExample();
    }

    // 静态工厂方法
    public static SingletonExample getInstance() {
        return singletonExample;
    }

}

4、枚举方式,线程安全,推荐的方式。

相比于懒汉模式,在安全性方面更容易保证,在饿汉模式,在安全性方面,在实际调用方面才可以初始化,不会造成资源的浪费。

public class SingletonExample {
    // 私有的默认构造方法,避免外部通过new创建对象。
    private SingletonExample() {
    }

    // 静态工厂方法
    public static SingletonExample getInstance() {
        return Singleton.INSTANCE.getInstance();
    }

    // 枚举类,私有的枚举类。
    private enum Singleton {
        // instance
        INSTANCE;

        // 私有的类的实例
        private SingletonExample singletonExample;

        // JVM保证这个方法绝对只调用一次
        // 枚举类的构造方法
        Singleton() {
            singletonExample = new SingletonExample();
        }

        // 提供一个方法方便类来获取
        public SingletonExample getInstance() {
            // 返回枚举类里面的实例
            return singletonExample;
        }
    }
}

喜迎
春节