设计模式--单例模式

ぃ、小莉子 提交于 2020-01-28 04:09:14

基本概念

定义

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

类型

创建型

适用场景

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

比如:

  • 单服务情况下的计数器可以用单例,但是集群就需要用共享
  • 线程池、连接池
  • 配置

优点

  • 内存中只有一个实例,减少了内存开销
  • 避免对资源的多重占用(比如说文件需要避免重复打开导致同时写)
  • 设置全局的访问点,严格控制访问

缺点

  • 没有接口,扩展困难,如果要修改,肯定要修改代码

需要注意的事情

  • 私有化构造器
  • 线程安全(非常重要)
  • 延迟加载(非常重要)
  • 序列化和反序列化安全的问题
  • 反射(防止反射攻击)

相关的设计模式

  • 单例模式和工厂模式
  • 单例模式和享元模式

1. 懒汉式

/**
 * @Classname LazySingleton
 * @Description 线程不安全的懒汉式
 * @Date 2019/12/22 15:58
 * @Author Cheng
 */
public class LazySingleton {
    private static LazySingleton lazySingleton = null;

    private LazySingleton() {
    }

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

利用idea线程级别debug触发并发情况下的问题

  1. getInstance()中的判断处打断点
  2. 当线程1进行至断点处,单步进入,当并不让其完成初始化
  3. 切换至线程2,依然可以通过if判断进入
  4. 之后继续,两个线程便分别创建了一个实例

Thread-0 singleton.LazySingleton@131c736d

Thread-1 singleton.LazySingleton@3dfb47dd

2. 懒汉式改进–类锁

加锁之后,未获取锁的线程将无法进入接下来的逻辑,状态变成monitor
在这里插入图片描述

写法一:

/**
 * @Classname LazySingleTon2
 * @Description 加上类锁保证线程安全
 * @Date 2019/12/22 16:20
 * @Author Cheng
 */
public class LazySingleTon2 {
    private static LazySingleTon2 instance = null;

    private LazySingleTon2() {}

    public synchronized static LazySingleTon2 getInstance() {
        if (instance == null) {
            instance = new LazySingleTon2();
        }
        return instance;
    }
}

写法二:

/**
 * @Classname LazySingleTon2
 * @Description 加上类锁保证线程安全
 * @Date 2019/12/22 16:20
 * @Author Cheng
 */
public class LazySingleTon2 {
    private static LazySingleTon2 instance = null;

    private LazySingleTon2() {}

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

3. double check

!!重要,double check为什么要这么写,最重要的是为什么需要volatile

在懒汉加锁的方式中,我们保证了线程的安全,但是同样也导致了效率的低下,先分析一下为什么会导致效率低下

当我们调用getInstance()方法的时候,可能有两种情况

  1. instance为空,需要创建
  2. instance不为空,不需要创建

在第二种情况当中,也就是不需要创建实例的时候,是不存在线程安全问题的,但是在懒汉加锁的模式种,我们对这种情况依然加了锁,这才导致了效率的低下,毕竟绝大多数的时候是不需要创建实例的

那么,我们的目标就比明确了

  1. 不需要创建实例的时候不加锁,保证效率
  2. 需要创建实例的是加锁保证线程安全

然后我们的整个设计思路也很清晰,其实就是在懒汉加锁模式外面又加了一层判断

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

这样一来,其实我们已经实现了最初的构想,但是这其中还有一个,这就是我们需要volatile的原因

仔细想一想,在之前的懒汉加锁模式中,其实只有一种方式跑完getInstance的代码,因为我们在一开始就加了锁,而在上面这段代码中,getInstance方法只有一部分处于临界区,就是加锁的那部分,所以在并发时,其实有可能同时有两个线程处于getInstance方法中,这就给接下来的问题埋下了伏笔!!

另一个问题就是关于指令重排序的问题,当我们new一个对象的时候,代码看起来只有一行,但其实包括了三个步骤:

  1. 给对象分配内存
  2. 初始化对象
  3. 将变量指向分配好的内存地址

其中第2、3两个步骤是可能会被编译器重排序的(出于性能优化的目的),现在来假设这么一种情况:

  1. 指令2、3被重排序了
  2. 线程1运行完“将变量指向分配好的内存地址 ”,但此时对象尚未初始化完成
  3. 线程2运行到了外层的if(instance == null),此时变量已经有了引用地址,所以null判断为false,将直接返回instance实例
  4. 程序调用了实例,但是其实这个实例并没有初始化完成,所以将触发异常

相应的,我们有两种解决方案:

  1. 禁止重排序
  2. 重排序对其余线程不可见

volatile就是第一种解决方案的实现策略————禁止指令重排序

4. 静态内部类

/**
 * @Classname StaticInnerClassSingleton
 * @Description 静态内部类实现的单例模式
 * @Date 2019/12/22 17:56
 * @Author Cheng
 */
public class StaticInnerClassSingleton {
    private static class InnerClass{
        private static StaticInnerClassSingleton instance = new StaticInnerClassSingleton();
    }

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

首先了解这么一个情况:JVM在初始化类的时候会获取一个锁,来避免多个线程对同一个类的重复初始化,这里还需要了解一个情况,我们的innerClass定义了一个内部类,但是并没有任何变量指向该类,所以在调用getInstance方法之前,innerClass其实是并没有被初始化的。

这样一来,非构造线程就无法看到指令的重排序,那我们的目的也就达到了

附:类(接口)被初始化的条件:

  1. 类实例被创建
  2. 类中声明的一个静态方法被调用
  3. 类中声明的一个静态成员被赋值
  4. 类中声明的一个静态成员被使用,并且该成员不是一个常量成员
  5. 类是顶级类,且类中有嵌套的断言语句(不常配到)

5. 饿汉式

缺点就是在类初始化的是就已经创建了实例,一直占用内存,即使没有用到

/**
 * @Classname HungrySingleton
 * @Description 饿汉式单例模式
 * @Date 2019/12/22 18:11
 * @Author Cheng
 */
public class HungrySingleton {
    // final修饰的变量必须在类初始化完成时已经赋值,所以懒汉式是不能加final的
    private final static HungrySingleton instance = new HungrySingleton();

//    也可以用静态代码块赋值
//    static{
//        instance = new HungrySingleton();
//    }
    private HungrySingleton() {}

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

6. Enum枚举类单例模式

枚举类的单例模式可能是最好的实现模式,经过序列化和反序列化之后的对象仍然是同一个实例,而且其成员变量也是相等的

/**
 * @Classname EnumInstance
 * @Description 枚举类的单例模式
 * @Date 2019/12/22 19:18
 * @Author Cheng
 */
public enum EnumInstance {
    INSTANCE;
    private Object data;

    public Object getData() {
        return data;
    }

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

    public static EnumInstance getInstance() {
        return INSTANCE;
    }
}

反序列化对枚举类的处理:readEnum

  • 反序列化的时候对于枚举常量,根本就不会创建新的对象,所以对枚举类单例模式根本没影响

反射对枚举类的影响

  • 获取不了无参构造器,enum类只有一个构造器,还是需要两个参数的
  • 不能反射创建枚举对象,因为newInstance方法会判断是否是枚举类,如果是就抛异常了
  • 反编译发现enum是类似于饿汉式的,没有延时初始化

7. 单例模式的破坏:反序列化之后的实例和原来的实例相同吗

我们获取一个实例,然后将该实例序列化到文件中,之后在从文件中反序列化,之后会发现,新的实例和原来的实例不相等,这就违背了单例模式的设计初衷——在任何情况下都只有一个实例

public class Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        HungrySingleton instance = HungrySingleton.getInstance();
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singletonFile"));
        oos.writeObject(instance);
        File file = new File("singletonFile");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        HungrySingleton newInstance = (HungrySingleton) ois.readObject();
        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance==newInstance);
    }
}

输出:
singleton.HungrySingleton@14ae5a5
singleton.HungrySingleton@6d03e736
false

解决方案
非常简单,只需要在单例类的代码中添加如下代码:

private Object readResolve(){
    return instance;
}

原理

  • 反序列化的源码中是通过反射获取的新对象,所以与我们之前的实例不同
  • 方法名也是在源码里写明的,有一处就是判断是否实现了readResolve方法
  • 在这个过程中,虽然返回的是同一个单例,但是确实也创建了一个新的对象

8. 单例模式反射攻击

原理:可以通过反射调用构造器**

public static void main(String[] args){
    Class objectClass = HungrySingleton.class;
    Constructor constructor = objectClass.getDeclaredConstructor();
    // 将私有化的构造器打开访问权限
    constructor.setAccessible(true);
    HungrySingleton instance = HungrySingleton.getInstance();
    HungrySingleton newInstance = (HungrySingleton) constructor.newInstance();
    System.out.println(instance);
    System.out.println(newInstance);
    System.out.println(instance==newInstance);
}

显然两个实例时不相等的

解决办法

对于静态内部类的单例模式以及饿汉式单例模式,他们都是在类初始化的时候就把实例创建好了,对此,我们可以在构造器中做一些处理来进行防御

private HungrySingleton() {
    if(instance!=null)
        throw new RuntimeException("单例构造器禁止反射调用");
}

而对于在调用getInstance方法时才创建实例的形式来说,情况就有些复杂

  1. 如果先是正常线程调用,然后是反射线程调用,那么之前的防御性代码依然有效
  2. 如果先是反射线程调用,然后是正常线程调用,则会产生不同的实例

对此,我们可能会考虑加一些标志位之类的做一些处理,但是,反射是基本可以修改一切的,所以,对于这种lazy-load类型的单例模式,只要反射线程先进入,那就只能创建多个实例了,毕竟防不胜防啊~~

9. 容器单例

  • 适合程序初始化时候实例化多个单例对象,多线程情况下是由隐患的,即使是ConcurrentHashmap也是有隐患的
  • hashtable是可以的,但是性能较差
  • 所以需要根据业务来判断是否用,找一个平衡
/**
 * @Classname ContainerSingleton
 * @Description TODO
 * @Date 2019/12/22 19:38
 * @Author Cheng
 */
public class ContainerSingleton {
    private static Map<String, Object> sigletonMap = new HashMap<>();

    public static void putInstance(String key, Object instance) {
        if (key != null && key.length() >=0 && instance != null) {
            if (!sigletonMap.containsKey(key)) {
                sigletonMap.put(key, instance);
            }
        }
    }

    public static Object getInstance(String key) {
        return sigletonMap.get(key);
    }
}

10. ThreadLocal线程单例模式

  • 只保证同一个线程会一直拿到同一个实例
/**
 * @Classname ThreadLocalInstance
 * @Description TODO
 * @Date 2019/12/22 19:50
 * @Author Cheng
 */
public class ThreadLocalInstance {
    private static final ThreadLocal<ThreadLocalInstance> threadLocalInstanceThreadLocal
            = new ThreadLocal(){
        @Override
        protected Object initialValue() {
            return new ThreadLocalInstance();
        }
    };

    private ThreadLocalInstance() {

    }

    public static ThreadLocalInstance getInstance() {
        return threadLocalInstanceThreadLocal.get();
    }
}

11. 单例模式的一些应用

  • spring中的AbstractFactoryBean中的getObject方法
  • MyBatis中的ErrorContextinstance方法用的就是ThreadLocal方式让每个线程维护自己的上下文
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!