单例模式以及常见的两种实现模式

单例模式是校招中最常考的设计模式之一.

设计模式其实就是类似于“规章制度”,按照这个套路来进行操作。

单例模式能保证某个类在程序中只存在唯一 一份实例。而不会创建出多个实例,如果创建出了多个实例,就会编译报错。而不会创建出多个实例,如果创建出了多个实例,就会编译报错。不使用单例模式也可以做到,就像跟别人借钱说我一定会还一样,但是模式就相当于打了欠条,一定得做到的。这一点在很多场景上都需要. 比如 JDBC 中的 DataSource 实例就只需要一个。

单例模式具体的实现方式, 分成 "饿汉" 和 "懒汉" 两种.

饿汉模式

类加载的同时, 创建实例(给人一种很急的感觉)

// 饿汉模式的 单例模式 实现.
// 此处保证 Singleton 这个类只能创建出一个实例.
class Singleton {
    // 在此处, 先把这个实例给创建出来了.
    private static Singleton instance = new Singleton();

    // 如果需要使用这个唯一实例, 统一通过 Singleton.getInstance() 方式来获取.
    public static Singleton getInstance() {
        return instance;
    }

    // 为了避免 Singleton 类不小心被复制出多份来.
    // 把构造方法设为 private. 在类外面, 就无法通过 new 的方式来创建这个 Singleton 实例了!!
    private Singleton() {}
}

public class ThreadDemo19 {
    public static void main(String[] args) {
        Singleton s = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();

        // Singleton s3 = new Singleton();
        System.out.println(s == s2);
    }
}

        运行一个 Java 程序,会先让 Java 进程找到并读取对应的 .class 文件,就会读取文件内容并解析,构造成类对象......这一系列的过程操作就叫做 类加载。 

        因为 static 修饰的变量落入到了类对象里面,又因为类对象是在类加载阶段内创建出来的唯一一个实例,同时构造方法是 private 修饰的,因此就只有这一个实例的成员了。

懒汉模式

类加载的时候不创建实例,第一次使用的时候才创建实例,如果不用就不创建了(效率更高了)

class SingletonLazy {
    private static SingletonLazy instance = null;

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

    private SingletonLazy() {}
}

public class ThreadDemo20 {
    public static void main(String[] args) {
        SingletonLazy s = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
        System.out.println(s == s2);
    }
}

         上述的这两种模式,饿汉模式只涉及到“读操作”,懒汉模式既涉及到“读操作”也涉及到“写操作”,因此这个在多线程环境下会有线程安全问题。

因此加上 synchronized 可以改善这里的线程安全问题。

public static SingletonLazy getInstance() {
        // 这一层 if 是因为只要对象被 new 了一次就不用再加锁产生更多开销了
        if (instance == null) {
            synchronized (SingletonLazy.class) {
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }

         也就是说,如果对象还有没有创建那么就要进行加锁,如果对象已经创建过了就不用加锁了,因为最后都是“读操作”,此时不加锁也没事。

懒汉模式-多线程版(改进)

        假设有很多线程都去进行 getInstance,这个时候就会出现内存可见性问题(编译器优化:只有第一次真正读了内存,后续都是读寄存器 / cache)

        同时还会有指令重排序问题:

instance = new Singleton();可以拆分成三个步骤

1.申请内存空间

2.调用构造方法,把这个内存空间初始化成一个合理的对象

3.把内存空间的地址赋值给 instance 引用

        正常情况下是1 2 3 顺序来执行的,但是编译器会为了提高效率从而调整顺序,可能就变成1 3 2,如果是单线程就没有区别。但在多线程环境下,假设 t1 是按照 1 3 2 执行的,当 t1 执行到 1 3 之后,准备执行 2 的时候,t2 跑过来执行了。此时在 t2 的角度 instance 就非空了,就会直接返回instance 了,但由于 t1 的 2 指令还没执行完,t2 拿到的是一个非法的对象(还没构造完成的不完整的对象),这时候如果尝试使用引用中的属性就会出现错误。例如 instance 里有个成员 num,构造方法是要初始化成100的,但是由于上述问题就导致构造方法还没执行,此时访问 num 是 0。

        因此加上 volatile 可以解决内存可见性问题和禁止指令重排序。

class SingletonLazy {
    private volatile static SingletonLazy instance = null;

    public static SingletonLazy getInstance() {
        if (instance == null) {
            synchronized (SingletonLazy.class) {  //加锁不是这个线程就一直赖着不走,而是切换调度正常,但是其他线程尝试加锁的时候就会阻塞。
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }

    private SingletonLazy() {}
}

public class ThreadDemo20 {
    public static void main(String[] args) {
        SingletonLazy s = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
        System.out.println(s == s2);
    }
}

理解双重 if 判定 / volatile:

加锁 / 解锁是一件开销比较高的事情. 而懒汉模式的线程不安全只是发生在首次创建实例的时候.

因此后续使用的时候, 不必再进行加锁了.

外层的 if 就是判定下看当前是否已经把 instance 实例创建出来了.

同时为了避免 "内存可见性" 导致读取的 instance 出现偏差, 于是补充上 volatile .

当多线程首次调用 getInstance, 大家可能都发现 instance 为 null, 于是又继续往下执行来竞争锁,

其中竞争成功的线程, 再完成创建实例的操作.

当这个实例创建完了之后, 其他竞争到锁的线程就被里层 if 挡住了. 也就不会继续创建其他实例.

1) 有三个线程, 开始执行 getInstance , 通过外层的 if (instance == null) 知道了实例还没

有创建的消息. 于是开始竞争同一把锁.

2) 其中线程1 率先获取到锁, 此时线程1 通过里层的 if (instance == null) 进一步确认实例是

否已经创建. 如果没创建, 就把这个实例创建出来.

3) 当线程1 释放锁之后, 线程2 和 线程3 也拿到锁, 也通过里层的 if (instance == null) 来

确认实例是否已经创建, 发现实例已经创建出来了, 就不再创建了.

4) 后续的线程, 不必加锁, 直接就通过外层 if (instance == null) 就知道实例已经创建了, 从

而不再尝试获取锁了. 降低了开销.

  • 27
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值