单例模式 (饿汉、懒汉)

x33g5p2x  于2021-12-09 转载在 其他  
字(3.7k)|赞(0)|评价(0)|浏览(180)

定义

单例模式,是一种常见的"设计模式"

设计模式: 设计模式是一套经过反复使用的代码设计经验,目的是为了重用代码、让代码更容易被他人理解、保证代码可靠性

单例模式,场景: 代码中,有些概念,不应该存在多个实例,此时应该使用单例模式来解决
例: MySQL JDBC中,第一步就是创建一个 DataSourse 对象,DataSourse 对象,在一个程序中只有一个实例,不应该实例化多份DataSourse 对象
可以用单例模式来解决这种场景,保证指定的类只有一个实例 (若尝试创建多个实例,直接编译就会报错)

单例模式的实现

饿汉模式

类加载的同时,创建实例
(只要类被加载,就会立刻实例化 Singleton 实例)

代码:

public class ThreadDemo22 {
    /* * 饿汉模式 单例实现 * "饿" —— 只要类被加载,实例就会立刻被创建 (实例创建的时机比较早) * */
    static class Singleton{
        // 把构造方法变成私有的,此时在该类的外部就无法 new 这个类的实例了
        private Singleton(){

        }
        // 再来创建一个 static 的成员,表示 Singleton 类唯一的实例
        // static 成员 和类相关,和实例是无关的
        // 类在内存中只有一份,static 成员也只有一份
        private static Singleton instance = new Singleton();
        public static Singleton getInstance(){
            return instance;
        }
    }

    public static void main(String[] args) {
        // 此处 new 可以,是因为 Singleton 是 ThreadDemo22 的内部类,
        // ThreadDemo 是可以访问 内部类的 private 成员的
        Singleton s = new Singleton();
        
        // 此处的 getInstance 就是获取该类实例的唯一方式,不应该使用其他方式来创建实例
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        System.out.println(s1 == s2);
    }
}

输出结果:

懒汉模式

当类被加载的时候,不会立刻实例化
等到第一次使用这个实例的时候,再实例化

public class ThreadDemo23 {
    /* * 懒汉模式 * */
    static class Singleton{
        private Singleton() {

        }
        // 类加载的时候,没有立刻实例化
        // 第一次调用 getInstance 时,才真正的实例化
        private static Singleton instance = null;

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

类加载的时候,没有立刻实例化;第一次调用 getInstance 时,才真正的实例化

若代码,一直没有调用 getInstance,此时实例化的过程也就被省略掉了 —— 延时加载
一般认为,“懒汉模式” 比 “饿汉模式” 的效率更高~
原因: 懒汉模式有很大的可能是 “实例是用不到”,此时就节省了实例化的开销

线程安全问题分析:

思考:****“饿汉模式” 和 “懒汉模式”,哪个是线程安全的???
(线程安全:假设多个线程并发的调用 getInstance 方法,是否会导致逻辑错误)

啥样的情况会导致线程不安全???

在线程安全问题,我们提到有以下原因:

  • 线程是抢占式执行的
  • 修改操作不是原子的
  • 多个线程尝试修改同一个变量(单例模式常出现)
  • 内存可见性
  • 指令重排序

饿汉模式—线程安全;
懒汉模式—线程不安全

分析:
1.饿汉模式:
实例化时机是在类加载的时候,而类加载只有一次机会,不可能并发执行
当多线程并发的调用 getInstance 时,由于 getInstance 里只做了一件事:读取 instance 实例的地址,相当于多个线程同时读取同一个变量;因此,饿汉模式是线程安全的

2.懒汉模式:
多线程同时调用 getInstance 时,getInstance 中做了四件事:①读取 instance 的内容;②判断是否为null;③若 instance 为 null,就 new 实例;④返回实例的地址
当 new 实例的时候,就会修改 instance 的值

画图分析:

懒汉模式,后续调用 getInstance 都不会触发线程安全问题,只有在第一次实例化的时候,多线程并发调用 getInstance 时,会有线程不安全问题的风险

如何解决线程安全问题??

那么,如何改进 懒汉模式,让代码变成线程安全的???

方法1— 加锁 synchronized

  • 改法1

这样写,读取判断操作,和new 修改操作 仍然不是原子的,故这样修改不可行!!

private static Singleton instance = null;

public static Singleton getInstance(){
	if(instance == null){
		synchronized (Singleton.class){
			instance = new Singleton();
		}
	}
	return instance;
}
  • 改法2

这么加是可以保证原子性的

private static Singleton instance = null;

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

上述改法,虽然解决了线程不安全的问题,但仍然会问题 — 效率问题,

画图分析:

  • 改法3
private static Singleton instance = null;

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

画图和改法2 差不多,只不过 return 操作是在释放锁内部来完成的
由于 return 只是在读,所以这个操作放到锁里边或者锁外边不影响结果

虽然改法2 和 改法3 都可行,但是改法2 的锁粒度更小,改法3 的锁粒度更大
锁的粒度: 锁中包含的代码越多,就认为锁粒度越大
一般,我们希望锁的粒度小一点更好,因为锁的粒度越大,说明这段代码的并发能力就越受限

方法2 — 双重 if

由于加锁是为了避免第一次创建实例时线程不安全,后面在进行加锁解锁操作都只会降低性能,所以外层再添加 if 判断,当发现其为空时才加锁,否则直接返回已经创建好的实例对象,减少了加锁解锁的次数,从而提高性能

private static Singleton instance = null;

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

画图分析:

  • 实例化之前:

此处有多个读操作,可能会被编译器优化:只有第一次读,才从内存中读,后续的读就是从CPU中读取寄存器(上次读到的结果)
这样就可能导致线程1 修改之后,线程2 没有读到最新的值

  • 实例化之后:

为了改进上述可能出现的编译器优化的问题,再添加 volatile

方法3 — volatile

private volatile static Singleton instance = null;

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

懒汉模式的最终优化结果:

static class Singleton {
    private Singleton() {

    }

    //创建static成员变量,标识Singleton类的唯一实例,为避免内存可见性问题,添加volatile
    private volatile static Singleton instance = null;

    public static Singleton getInstance() {
        // 加锁是为了避免第一个创建实例时线程不安全,后面在进行加锁解锁操作都只会降低性能
        if (instance == null) {
            
            //如果为空,说明实例还未存在(即第一次使用),则创建实例
            //加锁,确保判断为空和 new对象两个操作 成为原子操作
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
关键点总结

1.加锁 — 保证线程安全
2.双重 if — 保证效率
3.volatile — 避免内存可见性引发的问题

以上三点缺一不可

相关文章