• 聊透常见设计模式之单例模式


    设计模式定义:

    设计模式(Design pattern)是可复用面向对象软件的基础,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。

    设计模式分类

    总体来说设计模式分为三大类

    1. 创建型模式(5种)

      工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式

    2. 结构型模式(7种)

      适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式

    3. 行为型模式(11种)

      策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式

    单例模式

    定义

    保证一个类仅有一个实例,并提供一个该实例的全局访问点。——《设计模式》

    为什么需要?

    系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能。

    单例模式种类

    1.懒汉式V1

    1. public class Singleton {
    2. private static Singleton instance;
    3. private Singleton (){} // 构造器私有
    4. public static Singleton getInstance() {
    5. if (instance == null) { // 1
    6. instance = new Singleton(); // 2
    7. }
    8. return instance;
    9. }
    10. }

    这种方式不可用 因为线程不安全
    原因是在上面1处的代码如果在多线程下,一个线程进入了 if (singleton == null) 判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式

    2.懒汉式V2(线程安全)

    1. public class Singleton {
    2. private static Singleton singleton;
    3. private Singleton() {}
    4. public static synchronized Singleton getInstance() {
    5. if (singleton == null) {
    6. singleton = new Singleton();
    7. }
    8. return singleton;
    9. }
    10. }

    这种方式线程安全getInstance() 方法进行了线程同步,但是这种方式不推荐使用

    缺点:

    效率太低了,每个线程在想获得类的实例时候,执行 getInstance() 方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接 return 就行了。方法进行同步效率太低要改进。

    3.双重校验锁

    1. public class Singleton {
    2. private static volatile Singleton singleton; // 1
    3. private Singleton() {} //私有构造函数
    4. //静态工厂方法
    5. public static Singleton getInstance() {
    6. if (singleton == null) { //双重检测机制
    7. synchronized (Singleton.class) { //同步锁
    8. if (singleton == null) { //双重检测机制
    9. singleton = new Singleton();
    10. }
    11. }
    12. }
    13. return singleton;
    14. }
    15. }

    双重校验锁线程安全推荐使用

    疑问:

    1.这种方式为什么要检查两次?检查一次行不行?

    检查一次不行

    第一次判断singleton == null作用是,如果单例对象已经创建,就直接返回对象,不需要进入同步锁,提高效率(我感觉这一次检查不是必须,但是推荐)

    第二次判断singleton == null,假如没有这个判断,多个线程同时通过第一次singleton == null之后,只有一个线程进入临界区,其他线程排队,没有第二次检查的话,会创建多个实例,第二次是必须检查

    2:在代码1处使用了关键字volatile,为什么需要?

    因为 JVM 指令重排序,也就是并不限制处理器的指令顺序,说白了就是在不影响结果的情况下,顺序可能会被打乱。

    在执行sInstance = new Singleton();这条命令语句时,JMM并不是一下就执行完毕的,即不是原子性,实质上这句命令分为三大部分:

    1. 为对象分配内存
    2. 执行构造方法语句,初始化实例对象
    3. 把sInstance的引用指向分配的内存空间

    在JMM中这三个步骤中的2和3不一定是顺序执行的,如果线程A执行的顺序为1、3、2,在第2步执行完毕的时候,恰好线程B执行第一次判空语句,则会直接返回sInstance,那么此时获取到的sInstance仅仅只是不为null,实质上没有初始化,这样的对象肯定是有问题的!

    而volatile关键字的存在意义就是保证了执行命令不会被重排序,也就避免了这种异常情况的发生,所以这种获取单例的方法才是真正的安全可靠!

    4.饿汉式

    1. public class Singleton {
    2. private static Singleton instance = new Singleton();
    3. private Singleton (){}
    4. public static Singleton getInstance() {
    5. return instance;
    6. }
    7. }

    5.饿汉式其他变种1 静态常量

    1. public class Singleton {
    2. private static final Singleton INSTANCE = new Singleton();
    3. private Singleton (){}
    4. public static Singleton getInstance() {
    5. return INSTANCE;
    6. }
    7. }

    6.饿汉式其他变种2 静态代码块

    1. public class Singleton {
    2. private static Singleton instance;
    3. static {
    4. instance = new Singleton();
    5. }
    6. private Singleton (){}
    7. public static Singleton getInstance() {
    8. return instance;
    9. }
    10. }

    上面几种本质一样

    优点:无线程安全问题

    缺点:不能延迟加载,如果这个对象没有使用到,浪费内存

    7.静态内部类

    1. public class Singleton {
    2. private Singleton() {}
    3. private static class SingletonInstance {
    4. private static final Singleton INSTANCE = new Singleton();
    5. }
    6. public static Singleton getInstance() {
    7. return SingletonInstance.INSTANCE;
    8. }
    9. }

    这种方式 避免了线程不安全,延迟加载,效率高

    8.枚举

    1. public enum EnumSingleton {
    2. INSTANCE;
    3. private Object object;
    4. EnumSingleton() {
    5. object = new Object();
    6. }
    7. public Object getInstance() {
    8. return object;
    9. }
    10. }

    优点:线程安全,能防止序列化破坏(后面说)

    思考:饿汉式/静态内部类/枚举是通用什么方式保证线程安全?

    以上的静态内部类、饿汉等模式均是通过定义静态的成员变量,以保证单例对象可以在类初始化的过程中被实例化。

    这其实是利用了ClassLoader的线程安全机制。ClassLoader的loadClass方法在加载类的时候使用了synchronized关键字。

    所以, 除非被重写,这个方法默认在整个装载过程中都是线程安全的。所以在类加载过程中对象的创建也是线程安全的。

    扩展讨论:

    那么这些方式都是和synchronized有关系的,有没有哪种方式可以完全不使用synchronized的吗?

    9.CAS(AtomicReference)实现单例模式

    1. public class Singleton {
    2. private static final AtomicReference INSTANCE = new AtomicReference();
    3. private Singleton() {}
    4. public static Singleton getInstance() {
    5. for (;;) {
    6. Singleton singleton = INSTANCE.get();
    7. if (null != singleton) {
    8. return singleton;
    9. }
    10. singleton = new Singleton();
    11. if (INSTANCE.compareAndSet(null, singleton)) {
    12. return singleton;
    13. }
    14. }
    15. }
    16. }

    优点:

    用CAS的好处在于不需要使用传统的锁机制来保证线程安全,CAS是一种基于忙等待的算法,依赖底层硬件的实现,相对于锁它没有线程切换和阻塞的额外消耗,可以支持较大的并行度。(CAS具体可以在JUC那块分享)

    缺点:

    CAS的一个重要缺点在于如果忙等待一直执行不成功(一直在死循环中),会对CPU造成较大的执行开销。

    另外,代码中,如果N个线程同时执行到 singleton = new Singleton();的时候,会有大量对象被创建,可能导致内存溢出。

    10.ThreadLocal实现单例模式

    1. public class Singleton {
    2. private static final ThreadLocal singleton =
    3. new ThreadLocal() {
    4. @Override
    5. protected Singleton initialValue() {
    6. return new Singleton();
    7. }
    8. };
    9. public static Singleton getInstance() {
    10. return singleton.get();
    11. }
    12. private Singleton() {}
    13. }

    这种方式仅了解,不推荐使用,因为它其实失去了单例的意义

    ThreadLocal 和 同步模式创建的单例区别:

    ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。对于多线程资源共享的问题,同步机制(synchronized)采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。这种是

    同步机制仅提供一份变量,让不同的线程排队访问,而ThreadLocal为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

    破坏单例模式的方式和解决办法

    1.反射破坏单例

    可以通过setAccessible(true)来绕过 private 限制,从而调用到类的私有构造函数创建对象。

    1. Singleton1 singleton1 = Singleton1.getInstance();
    2. Constructor cons = Singleton1.class.getDeclaredConstructor();
    3. cons.setAccessible(true);
    4. Singleton1 singleton2 = cons.newInstance(); // 注意 需要通过 Constructor newInstance
    5. System.out.println(singleton1);
    6. System.out.println(singleton2);

    结果

    1. com.pandacase.single.Singleton1@71bc1ae4
    2. com.pandacase.single.Singleton1@6ed3ef1

    解决办法?
    思路:解决的办法可以是在私有构造器中,判断是否已经存在单例对象了,如果存在,则抛出异常
    例如:

    1. public class Singleton {
    2. private static int count = 0;
    3. private static volatile Singleton singleton; // 1
    4. //私有构造函数
    5. private Singleton() {
    6. synchronized (Singleton.class) {
    7. if (count > 0){
    8. throw new RuntimeException("反射破坏单例对象.....");
    9. }
    10. count++;
    11. }
    12. }
    13. //静态工厂方法
    14. public static Singleton getInstance() {
    15. if (singleton == null) { //双重检测机制
    16. synchronized (Singleton.class) { //同步锁
    17. if (singleton == null) { //双重检测机制
    18. singleton = new Singleton();
    19. }
    20. }
    21. }
    22. return singleton;
    23. }
    24. }

    2.序列化破坏单例(前提是单例类实现了Serializable接口)

    一是可以将一个单例的实例对象写到磁盘,实现数据的持久化;二是实现对象数据的远程传输。 当单例对象有必要实现 Serializable 接口时,即使将其构造函数设为私有,在它反序列化时依然会通过特殊的途径再创建类的一个新的实例,相当于调用了该类的构造函数有效地获得了一个新实例!

    解决办法?

    1. /* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */
    2. public Object readResolve() {
    3. return instance;
    4. }

    3.对象克隆破坏单例

    clone()是 Object 的方法,每一个对象都是 Object 的子类,都有clone()方法。clone()方法并不是调用构造函数来创建对象,而是直接拷贝内存区域。因此当我们的单例对象实现了 Cloneable 接口时,尽管其构造函数是私有的,仍可以通过克隆来创建一个新对象,单例模式也相应失效了。

    解决办法?

    解决思想是,重写clone()方法,调clone()时直接返回已经实例的对象

    1. public class Singleton implements Cloneable{
    2. private static int count = 0;
    3. private static volatile Singleton singleton; // 1
    4. //私有构造函数
    5. private Singleton() {
    6. synchronized (Singleton.class) {
    7. if (count > 0){
    8. throw new RuntimeException("反射破坏单例对象.....");
    9. }
    10. count++;
    11. }
    12. }
    13. //静态工厂方法
    14. public static Singleton getInstance() {
    15. if (singleton == null) { //双重检测机制
    16. synchronized (Singleton.class) { //同步锁
    17. if (singleton == null) { //双重检测机制
    18. singleton = new Singleton();
    19. }
    20. }
    21. }
    22. return singleton;
    23. }
    24. // 重写 clone 方法
    25. @Override
    26. protected Object clone() throws CloneNotSupportedException {
    27. return singleton;
    28. }
    29. }

    单例模式在JDK中的应用

    java.lang.Runtime#getRuntime()

    应用场景

    1. 需要频繁的进行创建和销毁的对象;
    2. 创建对象时耗时过多或耗费资源过多,但又经常用到的对象;
    3. 工具类对象;
    4. 频繁访问数据库或文件的对象。
  • 相关阅读:
    Kafka常用参数
    linux 安装gradle7.4.2环境
    day42-反射01
    【Django笔记】 登录功能
    次氯酸(HClO)荧光探针 激发波长567 nm
    基于C++的朴素贝叶斯分类器
    Jquery 通过class名称属性,匹配元素
    Java项目:SSM自习室图书馆座位预约管理系统
    【面试题精讲】Java Stream排序的实现方式
    html中字体加粗
  • 原文地址:https://blog.csdn.net/LBWNB_Java/article/details/126314946