单例模式

2022-02-23  本文已影响0人  坤坤坤坤杨

使用一个私有构造方法、一个私有静态变量以及一个共有静态方法实现。
私有构造方法保证了不能通过构造方法来创建对象实例,只能通过公有的静态方法返回唯一的私有静态变量

1. 实现方式

image

1.1 懒汉式-线程不安全

public class Singleton {

    private static Singleton uniqueInstance;

    private Singleton() {
    }

    public static Singleton getUniqueInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
}

以上实现方式,私有静态变量被延迟实例化,好处是如果没有用到该类,那么就不会被实例化,节约资源,减少堆内存的使用。
但是在多线程情况下是有问题的,如果多个线程进入到if语句中,并且此时的uniqueInstance为null,那么会有多个线程实例化私有静态变量,导致存在多个实例化对象。

1.2 饿汉式-线程安全

private static Singleton uniqueInstance = new Singleton();

采用直接实例化的方式就不会产生线程安全的问题,但是这样的方式会导致资源的消耗,在加载类的时候并没有用到该实例化对象,这样就会在堆内存中开辟空间,占用内存。

1.3 懒汉式-线程安全

public static synchronized Singleton getUniqueInstance() {
    if (uniqueInstance == null) {
        uniqueInstance = new Singleton();
    }
    return uniqueInstance;
}

这样的方式保证了,同一时刻只有一个线程能够进入该方法,避免了被多次实例化的问题,但是这样又会有问题,使用同步方法,当有多个线程进来,一个线程执行了这个方法,其他线程会被阻塞,使用synchronized 的时候,加锁和释放锁都会带来用户态和内核态的切换,用户态和内核态的切换是一个非常消耗性能的操作

1.4 双重校验锁-线程安全

public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }

    public static Singleton getUniqueInstance() {
        if (uniqueInstance == null) {
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

加锁的操作只需要对实例化部分代码进行,只有当uniqueInstance没有被实例化时,才需要进行加锁。
为什么要双重判断?如果只有一个if语句会出现什么问题?
如果两个线程都进入了if语句,A线程判断uniqueInstance为null的情况下,B线程也判断为null这个时候,虽然有加锁的操作,但是两个线程都会执行new对象操作,那么就会有两个实例。
volatile 的作用是什么?
volatile 的作用有保证线程可见性,禁止操作指令重排,在这里就是禁止操作指令重排。在单线程中不会出现这种问题,但是多线程就会出现jvm指令重排序的问题:

因为 singleton = new Singleton() 这句话可以分为三步:
1. 为 singleton 分配内存空间;
2. 初始化 singleton;
3. 将 singleton 指向分配的内存空间。
但是由于JVM具有指令重排的特性,执行顺序有可能变成 1-3-2。这样在多线程下会导致一个线程获得一个未初始化的实例,如果要获取实例中的属性就会抛出异常。例如:线程T1执行了1和3,此时T2调用 getInstance() 后发现 singleton 不为空,因此返回 singleton, 但是此时的 singleton 还没有被初始化。
使用 volatile 会禁止JVM指令重排,从而保证在多线程下也能正常执行。

1.5 静态内部类实现

public class Singleton {

    private Singleton() {
    }

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

    public static Singleton getUniqueInstance() {
        return SingletonHolder.INSTANCE;
    }
}

当Singleton类加载时,静态内部类没有被加载进内存。只有当调用getUniqueInstance()方法从而触发SingletonHolder.INSTANCE时静态内部类才会被加载,此时初始化INSTANCE实例。
这种方式不均具有延迟初始化的好处,而且由虚拟机提供了对线程安全的支持。

1.6 枚举实现

public enum Singleton {
    uniqueInstance;
}

这是单例模式的最佳实践,它实现简单,并且在面对复杂的序列化或者反射攻击的时候,能够防止实例化多次。

public class Singleton implements Serializable {

    private static Singleton uniqueInstance;

    private Singleton() {
    }

    public static synchronized Singleton getUniqueInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
}

考虑以下单例模式的实现,该 Singleton 在每次序列化的时候都会创建一个新的实例,为了保证只创建一个实例,必须声明所有字段都是 transient,并且提供一个 readResolve() 方法。

如果不使用枚举来实现单例模式,会出现反射攻击,因为通过 setAccessible() 方法可以将私有构造函数的访问级别设置为 public,然后调用构造函数从而实例化对象。如果要防止这种攻击,需要在构造函数中添加防止实例化第二个对象的代码。

从上面的讨论可以看出,解决序列化和反射攻击很麻烦,而枚举实现不会出现这两种问题,所以说枚举实现单例模式是最佳实践。

上一篇下一篇

猜你喜欢

热点阅读