美文网首页
单例模式

单例模式

作者: 坤坤坤坤杨 | 来源:发表于2022-02-23 00:02 被阅读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,然后调用构造函数从而实例化对象。如果要防止这种攻击,需要在构造函数中添加防止实例化第二个对象的代码。

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

    相关文章

      网友评论

          本文标题:单例模式

          本文链接:https://www.haomeiwen.com/subject/dsynlrtx.html