设计模式---单例模式
一、概述
单例模式涉及到一个类,该类负责创建自身的对象,并且每次创建出来的对象都是同一个,同时对外提供获取该类唯一对象的方法
二、分类
单例模式分为两种
1、懒汉式 : 类加载的时候便会创建该类的对象
2、饿汉式 : 类加载的时候不会创建对象,只有在使用的时候才会去创建该类的对象
三、案例
3.1、饿汉式
通过静态成员变量或者静态代码块的方式实现
1、静态成员变量方式
public class Singleton {
    // 构造方法私有化,防止外界创建对象,因为每次通过 new 的方式创建的对象都会在堆内存中开辟一块空间,创建出来的对象就不是同一个了,违背了单例模式的定义
    private Singleton() {
        System.out.println("通过构造方法创建对象");
    }
    private static Singleton instance = new Singleton();
    public static Singleton getInstance() {
        return instance;
    }
}
// 测试类
public class SingletonDemo {
    public static void main(String[] args) {
        for (int i = 1; i <= 5; i++) {
            Singleton instance = Singleton.getInstance();
            System.out.println(instance);
        }
    }
}
2、静态代码块方式
public class Singleton {
    private Singleton() {
        System.out.println("通过构造方法创建对象");
    }
    private static Singleton instance;
    static {
        instance = new Singleton();
    }
    public static Singleton getInstance() {
        return instance;
    }
}

静态成员变量和静态代码块方式实现单例模式本质上是一样的,通过上面的测试结果可以看出,构造方法被调用了一次,并且每次获取到的对象都是同一个,符合单例模式的要求
但是使用懒汉式单例模式有一个问题,比如有些对象我们暂时不需要使用,但是这些对象随着类的加载已经创建出来了,这势必会造成 内存浪费
3.2、懒汉式
饿汉式会造成内存空间的浪费,为了解决这个问题,我们还可以使用懒汉式的方式去实现单例模式,单例对象不随类的加载而创建,只有真正需要使用的时候才会去创建
public class Singleton {
    private Singleton() {
        System.out.println("通过构造方法创建对象");
    }
    private static Singleton instance;
    public static Singleton getInstance() {
        // 对象如果已经创建了则直接返回该单例对象
        if(Objects.isNull(instance)){      // 代码 A
            instance = new Singleton();    // 代码 B
        }
        return instance;
    }
}
这种方式在单线程下是可行的,但是在并发条件下会有线程安全问题
线程 1 执行了代码 A,判断 instance 为空,在准备执行代码 B 的时候,CPU 的执行权被线程 2 抢占,此时 instance 仍为空
线程 2 获取到 CPU 执行权,执行代码 A,发现 instance 为空,然后执行代码 B,调用 new Singleton() 创建了一次对象,然后 CPU 的执行权又被线程 1 抢占了
线程 1 重新获取到 CPU 的执行权,执行代码 B,又调用构造方法,创建了 Singleton 的对象
可以看出,构造方法被调用两次,违背了单例模式的定义(每次创建出来的对象都是同一个)
// 测试代码
public class SingletonDemo {
    public static void main(String[] args) {
        Runnable runnable = () -> {
            Singleton instance = Singleton.getInstance();
            System.out.println("创建的对象内存地址值为: " + instance + "当前执行的线程是: " + Thread.currentThread().getName());
        };
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(runnable);
            thread.start();
        }
    }
}

从结果可以看出,5 个线程调用了 5 次构造方法,创建出了 5 个不同 Singleton 对象,违背了单例模式原则,那么怎么解决并发条件下的线程安全问题呢,
可以使用 Synchronized 关键字
public class Singleton {
    private Singleton() {
        System.out.println("通过构造方法创建对象");
    }
    private static Singleton instance;
    public synchronized static Singleton getInstance() {
        // 对象如果已经创建了则直接返回该单例对象
        if(Objects.isNull(instance)){
            instance = new Singleton();
        }
        return instance;
    }
}
 
从上面的测试结果能看出,构造方法被调用一次,创建的对象都是同一个,符合单例模式,但是通过在方法上使用 synchronized 的方式会存在性能问题,多个线程调用该方法创建 Singleton 对象的时候都在同步等待,多线程就失去了意义
我们可以尝试缩小锁的粒度
public class Singleton {
    private Singleton() {
        System.out.println("通过构造方法创建对象");
    }
    private static Singleton instance;
    public static Singleton getInstance() {
        synchronized (Singleton.class) {       // 代码 A
            if (Objects.isNull(instance)) {    // 代码 B
                instance = new Singleton();    // 代码 C
            }
        }
        return instance;
    }
}
这样就不需要同步整个方法了,只有在创建对象的时候才去同步,效率提高了,但是这样仍然会存在问题
线程 1 执行到代码 B 的时候,CPU 执行权发生切换
线程 2 获取到了 CPU 执行权,执行代码 A 的时候只能同步等待,CPU 执行权发生切换
线程 3 获取到了 CPU 执行权,执行代码 A 的时候也只能同步等待
同步等待和锁竞争的问题仍然没有解决,那么还有没有更好的办法呢?
public class Singleton {
    private Singleton() {
        System.out.println("通过构造方法创建对象");
    }
    private static Singleton instance;
    public static Singleton getInstance() {
        // 假设有 10 个线程同时调用 getInstance() 方法创建单例对象,此处判断如果已经存在 instance 对象,那么直接返回
        // 没有则继续创建,降低了多线程环境下的锁竞争问题
        if (Objects.isNull(instance)) {
            // 并发条件下,经过 Objects.isNull(instance) 过滤后,只会有少量的线程能执行到这里,假设最终只剩下 线程 1、线程 2、线程 3,这样就降低了需要同步等待的线程数量
            synchronized (Singleton.class) {
                if (Objects.isNull(instance)) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
10 个线程,最终只有线程 1、2、3 需要同步等待,降低了锁竞争次数,是不是这样就完美的解决了所有问题呢
其实并不是,一个对象的创建包含 3 个部分
1、在堆中为 Singleton 对象开辟内存空间
2、调用构造方法初始化对象,为成员变量赋值
3、将内存地址值指向堆中分配的内存空间
正常情况下依次执行步骤 1、2、3 之后,对象才算是真正的创建完成
但是步骤 2 与步骤 3 没有数据依赖关系,执行顺序是不确定的,如果先执行了步骤 3,然后再执行步骤 2,此时获取到的 Singleton 对象不为空,但是属性为空,对于属性,基本类型会有默认值,引用类型是 null,在具体业务使用对象中属性的时候就有可能抛出空指针异常
为了解决这个问题,需要禁止指令重排,这个时候就需要使用 volatile 关键字
public class Singleton {
    private Singleton() {
        System.out.println("通过构造方法创建对象");
    }
    // volatile 禁止指令重排
    private static volatile Singleton instance;
    public static Singleton getInstance() {
        if (Objects.isNull(instance)) {
            synchronized (Singleton.class) {
                if (Objects.isNull(instance)) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
                    
                
                
            
        
浙公网安备 33010602011771号