彻底搞懂单例模式【一篇就够】

一、【饿汉模式】- 多线程安全单例模式实例一(不使用同步锁)

缺点:对象在没有使用之前就已经初始化了。这就可能带来潜在的性能问题:如果这个对象很大呢?没有使用这个对象之前,就把它加载到了内存中去是一种巨大的浪费。另外,当系统中这样的类较多时,会使得启动速度变慢 。

public class EagerSingleton {
    private static final EagerSingleton singleton = new EagerSingleton();

    private EagerSingleton() {
    }

    public static EagerSingleton getSingleton() {
        return singleton;
    }
}

二、【懒汉模式】多线程安全单例模式实例二(使用同步方法)

缺点:一次锁住了整个方法,粒度有些大。改进--只在实例化语句加锁

public class LazySingleton {
    private static LazySingleton singleton = null;

    private LazySingleton() {
    }

    public static synchronized LazySingleton getSingleton() {
        if (singleton == null)
            singleton = new LazySingleton();
        return singleton;
    }
}

三、【懒汉模式】多线程安全单例模式实例三(使用同步方法-双重检查锁Double-Checked Lock)

双重检查的原因:可能会有多个线程同时同步块外的if判断语句,不进行双重检查就会导致创建多个实例。

同时因为singleton = new LazySingleton()不是原子操作,而是分为3个步骤进行:

①给singleton分配内存 ②在空间内创建对象即初始化 ③将singleton变量指向分配的内存空间

但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的②和③的顺序是不能保证的,最终的执行顺序可能是 ①②③也可能是 ①③②。如果是后者,则在③执行完毕、②未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后就会报错。
解决办法就是要将 singleton变量声明成 volatile 就可以了,因为volatile有禁止指令重排的特性。

Synchronized可依保证程序的有序性(即程序执行的顺序按照代码的先后顺序执行),但并无法禁止指令重排和处理器优化的。

[但是特别注意在 Java 5 以前的版本使用了 volatile 的双检锁还是有问题的。其原因是 Java 5 以前的 JMM (Java 内存模型)是存在缺陷的,即时将变量声明成 volatile 也不能完全避免重排序,主要是 volatile 变量前后的代码仍然存在重排序问题。这个 volatile 屏蔽重排序的问题在 Java 5 中才得以修复,所以在这之后才可以放心使用 volatile。]

public class LazySingleton {
    private volatile static LazySingleton singleton = null;

    private LazySingleton() {
    }

    public static LazySingleton getSingleton() {
        if (singleton == null) {
            synchronized (LazySingleton.class) {
                if (singleton == null)
                    singleton = new LazySingleton();
            }
        }

        return singleton;
    }
}

但是,实际还是可以通过反射方式创建多个实例:

public class SingletonTest {

    public static void main(String[] args) throws Exception{

        Class clz = Class.forName("LazySingleton");
        // "true" indicates that the reflected object should suppress(废弃) Java language access checking when it is used
        Constructor constructor = clz.getDeclaredConstructor();
        constructor.setAccessible(true);
        Object obj1 = constructor.newInstance();
        Object obj2 = constructor.newInstance();
    }
}

四、【懒汉模式】多线程安全单例模式实例四(使用静态内部类《Effective Java》上所推荐的)

外部类被加载时内部类并不需要立即加载内部类,内部类不被加载则不需要进行类初始化,因此单例对象在外部类被加载了以后不占用内存。

public class Singleton {
    private static class SingletonHolder {
        public static final Singleton instance = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }
}

五、单例模式实例五(实际没有用过,待验证)

我们可以通过EasySingleton.INSTANCE来访问实例,这比调用getInstance()方法简单多了。创建枚举默认就是线程安全的,所以不需要担心double checked locking,而且还能防止反序列化导致重新创建新的对象。但是还是很少看到有人这样写,可能是因为不太熟悉吧。

public enum EasySingleton{
    INSTANCE;
}

猜你喜欢

转载自blog.csdn.net/sjmz30071360/article/details/89357233
今日推荐