单例模式

Posted zhouxuan-c

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了单例模式相关的知识,希望对你有一定的参考价值。

定义

保证一个类仅有一个实例,并提供一个全局访问点

类型

创建型

适用场景

想确保任何情况下都绝对只有一个实例

优点

  1. 内存中仅有一个实例,减少内存开销。特别是一个对象需要频繁创建、销毁时,且创建、销毁时的性能无法优化。
  2. 可以避免对资源的多重占用,例如对文件进行写操作时
  3. 设置了全局访问点,严格控制访问。即对外不让 new, 只能通过方法来创建单例对象

缺点

没有接口,扩展困难

重点

  1. 私有构造器 (不让外界 new)
  2. 线程安全
  3. 延迟加载
  4. 序列号、反序列化安全
  5. 反射 (防御反射)

Coding

懒汉式


/**
 * 懒汉式: 延迟加载, 只有使用它时, 才初始化
 */
public class LazySingleton {

    private static LazySingleton lazySingleton = null;

    private LazySingleton() { }

    /**
     * 线程不安全, 多线程环境下.
     * 第一个线程走到 2 , 但还未执行好.
     * 第二个线程走到 1 , 此处一判断, 因为刚才那个还没 new, 所有结果时 true
     * 然后就 new 了两次, 最后返回的是后面一次的 new 出来
     */
    public static LazySingleton getInstance() {
        if (lazySingleton == null) {                            // 1
            lazySingleton = new LazySingleton();     // 2
        }
        return lazySingleton;
    }
}

然后,创建两个线程测试下。

public class T implements Runnable{

    @Override
    public void run() {
        LazySingleton lazySingleton = LazySingleton.getInstance();
        System.out.println(Thread.currentThread().getName() + " , " + lazySingleton);
    }

}
public class Test {

    public static void main(String[] args) {
        Thread t1 = new Thread(new T());
        Thread t2 = new Thread(new T());

        t1.start();
        t2.start();

        System.out.println("End");
    }

}

至于多线程 debug。右键小红点,选择 ‘Thread‘,然后走一个,点下拉框,就可以切换线程
技术图片
技术图片

多线程下出现问题,可以使用 synchronized 修饰

    public static LazySingleton getInstance() {
        synchronized (LazySingleton.class) {
            if (lazySingleton == null) {
                lazySingleton = new LazySingleton();
            }
        }
        return lazySingleton;
    }

但是同步锁比较消耗资源,而且 public synchronized static LazySingleton getInstance() 时,锁的是 class,范围挺大,对性能有一定影响。怎么在性能和安全性方面取得平衡呢?

双层检查的懒汉式

public class LazyDoubleCheckSingleton {

    private static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;

    private LazyDoubleCheckSingleton() { }

    public static LazyDoubleCheckSingleton getInstance() {
        if (lazyDoubleCheckSingleton == null) {
            synchronized (LazyDoubleCheckSingleton.class) {
                if (lazyDoubleCheckSingleton == null) {
                    // 此处注意指令重排序, 这一步可能会分为三部. 且 2 和 3 执行顺序可能会颠倒.
                    // 1. 分配内存给这个对象
                    // 2. 初始化对象
                    // 3. 设置 lazyDoubleCheckSingleton 指向刚分配的内存地址
                    lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
                }
            }
        }
        return lazyDoubleCheckSingleton;
    }

}

具体就是 if 判断时, lazyDoubleCheckSingleton 并不为 null, 因为前一个线程已经将 lazyDoubleCheckSingleton 已经指向一个内存空间。但是并没有初始化对象, 所以接下来直接 return 出去用就可能出现问题了

但注意,指令重排序,是不会影响单线程的执行结果的。(intra-thread semantics)

技术图片

怎么解决呢?

  1. 不允许重排序
  2. 允许一个线程的重排序,但是不让其他线程 "看到" 重排序

第一种解决方案,使用 volatile 修饰 lazyDoubleCheckSingleton 就行了.

private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;

在加上 volatile 后,重排序就会被禁止。多线程时,CPU 也有共享内存,所有的线程就都能看到共享内存的最新状态,保证了内存的可见性。(抱歉,关于 volatile 这里可能有错,我也没听懂,后面看并发编程再详学 TODO)

第二种解决方案,如何让线程一看不到线程零的重排序呢?使用静态内部类

public class StaticInnerClassSingleton {

    private StaticInnerClassSingleton() {}

    private static class InnerClass {
        private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
    }

    public static StaticInnerClassSingleton getInstance() {
        return InnerClass.staticInnerClassSingleton;
    }

}

技术图片

JVM 在类的初始化阶段,即 class 加载后,被线程使用之前,都是类的初始化阶段。顾名思义,这个阶段会执行类的初始化(过些日子学 JMV)。在执行类的初始期间,JVM 会去获取一个锁,这个锁可以同步多个线程对一个类的初始化,即上图中绿色部分。对于这个特性,我们可以实现基于静态内部类的且线程安全的延迟初始化方案。

在这种实现模式中,对于右侧的 2、3,也就是橙色的框。这两个的重排序,前面的线程 1并不会看到。即非构造线程,是不会看到重排序的。

当 线程0 和 线程1 试图获取锁的时候,这是肯定只有一个能获得,假设 线程0 获得,然后执行静态内部类的初始化。这时,即使2、3之间存在重排序。但是,线程1 是无法看到重排序的。因为有一个 class 对象的初始化锁。线程1 正在绿色区域等待。

饿汉式

在类加载时,就完成实例化。也因为在类初始化时就加载好了,所以是没有线程安全问题的。

public class HungrySingleton {

    private HungrySingleton() {}

    // 这里也可以把它放到静态代码块中
    private final static HungrySingleton hungrySingleton = new HungrySingleton();

    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }
    
}

使用序列号、反序列化破坏单例模式

工程中,一旦涉及序列号和反序列化的操作,请小心对单例的破坏

public class HungrySingleton implements Serializable {

    private HungrySingleton() {}

    private final static HungrySingleton hungrySingleton = new HungrySingleton();

    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }
   
}
public class Test {

    public static void main(String[] args) throws Exception {
        HungrySingleton instance = HungrySingleton.getInstance();
        ObjectOutputStream oos = new ObjectOutputStream(
                                    new FileOutputStream("singleton_file"));
        oos.writeObject(instance);

        File file = new File("singleton_file");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        HungrySingleton newInstance = (HungrySingleton) ois.readObject();

        System.out.println(instance);
        System.out.println(newInstance);

        // false
        System.out.println(instance == newInstance);
    }

}

这里没看明白,只知道反射啥的....(TODO)

反射攻击?

使用反射把构造器的私有权限打开,然后获取对象。

public class Test {

    public static void main(String[] args) throws Exception {
        Class objectClass = HungrySingleton.class;
        Constructor constructor = objectClass.getDeclaredConstructor();
        HungrySingleton instance = HungrySingleton.getInstance();
        // 置成 true,private权限就打开了. 注释掉就不能成功创建实例
        constructor.setAccessible(true);
        HungrySingleton newInstance = (HungrySingleton) constructor.newInstance();

        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }

}

如何防止反射?

    // 在私有构造方法中,加一个判断
    private HungrySingleton() {
        if (hungrySingleton != null) {
            throw new RuntimeException("单例构造器禁止反射.");
        }
    }

但是如果对象已存在,再用反射会抛出异常,如果对象不存在,还是可以正常通过反射创建对象。如果你想在类中使用信号量,不管是 int 还是 布尔。也是不行的,反射也可以把它改掉。


        // 修改 o1
        HungrySingleton o1 = HungrySingleton.getInstance();
        Field flag = o1.getClass().getDeclaredField("flag");
        flag.setAccessible(true);
        flag.set(o1, true);

Enum 枚举单例(推荐的)

public enum EnumInstance implements Serializable {

    INSTANCE;

    private Object data;

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    public static EnumInstance getInstance() {
        return INSTANCE;
    }

}
public class Test {

    public static void main(String[] args) throws Exception {
        EnumInstance enumInstance = EnumInstance.getInstance();
        enumInstance.setData(new Object());

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test"));
        oos.writeObject(enumInstance);

        File file = new File("test");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        EnumInstance newInstance = (EnumInstance) ois.readObject();

        System.out.println(enumInstance.getData());
        System.out.println(newInstance.getData());
        System.out.println(enumInstance.getData() == newInstance.getData());
    }

}

反编译后会看到

final 不能被继承
技术图片

构造器私有 不允许外部实例化
技术图片

类变量是静态 没有延迟初始化,通过静态块,在类加载时,就把对象的初始化完成。因此也是线程安全的
技术图片
技术图片

以上是关于单例模式的主要内容,如果未能解决你的问题,请参考以下文章

常用代码片段

性能比较好的单例写法

片段作为 Android 中的单例

单例片段或保存网页视图状态

你熟悉的设计模式都有哪些?写出单例模式的实现代码

单例模式以及静态代码块