关于后端:最简单的设计模式是单例

39次阅读

共计 5516 个字符,预计需要花费 14 分钟才能阅读完成。

单例模式能够说是 Java 中最简略的设计模式,但同时也是技术面试中频率极高的面试题。因为它不仅波及到设计模式,还包含了对于线程平安、内存模型、类加载等机制。所以说它是最简略的吗?
明天就别离从单例模式的实现办法和利用场景来介绍一下单例模式

一、单例模式介绍

1.1 单例模式是什么

单例模式也就是指在整个运行时域中,一个类只能有一个实例对象。

那么为什么要有单例模式呢?这是因为有的对象的创立和销毁开销比拟大,比方数据库的连贯对象。所以咱们就能够应用单例模式来对这些对象进行复用,从而防止频繁创建对象而造成大量的资源开销。

1.2 单例模式的准则

为了达到单例这个全局惟一的拜访点的成果,必须让单例满足以下准则:

  1. 阻止类被通过惯例办法实例化(公有构造方法)
  2. 保障实例对象的唯一性(以静态方法或者枚举返回实例)
  3. 保障在创立实例时的线程平安(确保多线程环境下实例只有一个)
  4. 对象不会被外界毁坏(确保在有序列化、反序列化时不会从新构建对象)

二、单例模式的实现形式

对于单例模式的写法,网上演绎的曾经有很多,然而感觉大多数只是列出了写法,不去解释为什么这样写的益处和原理。我偶尔在 B 站看了寒食君演绎的单例模式总结思路还不错,故这里借鉴他的思路来别离阐明这些单例模式的写法。

依照单例模式中是否线程平安、是否懒加载和是否被反射毁坏能够分为以下的几类

2.1 懒加载

2.1.1 懒加载(线程不平安)

public class Singleton {
    /** 保障构造方法公有,不被外界类所创立 **/
    private Singleton() {}
    /** 初始化对象为 null**/
    private static Singleton instance = null;

    public static Singleton getInstance() {
        // 判断是否被结构过,保障对象的惟一
        if (instance == null) {instance = new Singleton();
        }
        return instance;
    }
}

从下面咱们能够看到,通过 public class Singleton 咱们能够全局拜访该类;通过私有化构造方法,可能防止该对象被外界类所创立;以及前面的 getInstance 办法可能保障创建对象实例的惟一。

然而咱们能够看到,这个实例不是在程序启动后就创立的,而是在第一次被调用后才真正的构建,所以这样的提早加载也叫做 懒加载

然而咱们发现 getInstance 这个办法在多线程环境下是 线程不平安 的—如果有多个线程同时执行该办法会产生多个实例。那么该怎么办呢?咱们想到能够将该办法变成线程平安的,加上 synchronized 关键字。

2.1.2 懒加载(线程平安)

public class Singleton {
    /** 保障构造方法公有,不被外界类所创立 **/
    private Singleton() {}
    /** 初始化对象为 null**/
    private static Singleton instance;
    
    // 判断是否被结构过,保障对象的惟一, 而且 synchronize 也能保障线程平安
    public synchronized static Singleton getInstance() {if (instance == null) {instance = new Singleton();
        }
        return instance;
    }
}

然而咱们晓得,如果一个静态方法被 synchronized 所润饰,会把以后类的 class 对象锁住,会增大同步开销,升高程序的执行效率。所以能够从放大锁粒度角度去思考,把 synchronized 放到办法外面去,也就是让其润饰同步代码块,如下所示:

public class Singleton {
    /** 保障构造方法公有,不被外界类所创立 **/
    private Singleton() {}
    /** 初始化对象为 null**/
    private static Singleton instance;
    
    public static Singleton getInstance() {if (instance == null) {
            // 利用同步代码块,锁的是以后实例对象
            synchronized(Singleton.class) {instance = new Singleton();
            }
            
        }
        return instance;
    }
}

然而这个时候,咱们发现 if(instance == null) 是没有锁的,所以当两个线程都执行到该语句并都判断为 true 时,还是会排队创立新的对象,那么有没有新的解决形式?

2.1.3 懒加载(线程平安,双重检测锁)

public class Singleton {
    /** 保障构造方法公有,不被外界类所创立 **/
    private Singleton() {}
    /** 初始化对象 **/
    private static Singleton instance;

    public static Singleton getInstance() {
        // 第一次判断
        if (instance == null) {synchronized (Singleton.class) {
                // 第二次判断
                if (instance == null) {instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

咱们在上一节的代码上再加上一次判断,就是双重检测锁(Double Checked Lock, DCL)。然而上述代码也存在一些问题,比方在instance = new Singleton() 这行代码中,它并不是一个原子操作,实际上是有三步:

  • 给对象实例分配内存空间
  • new Singleton() 调用构造方法,初始化成员字段
  • instance对象指向调配的内存空间

所以会波及到内存模型中的指令重排,那么这个时候能够用 volatile关键字来润饰 instance对象,避免指令重排,写出如下代码:

public class Singleton {
    /** 保障构造方法公有,不被外界类所创立 **/
    private Singleton() {}
    /** 初始化对象, 加上 volatile 避免指令重排 **/
    private volatile static Singleton instance;

    public static Singleton getInstance() {
        // 第一次判断
        if (instance == null) {synchronized (Singleton.class) {
                // 第二次判断
                if (instance == null) {instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

此外,咱们也能够尝试应用一些乐观锁的形式达到线程平安的成果,比方 CAS。

2.1.4 懒加载(线程平安,CAS 乐观锁)

public class Singleton {private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<Singleton>();
    private static Singleton instance;
    
    private Singleton(){}
    public static final Singleton getInstance() {for(;;) {Singleton instance = INSTANCE.get();
            if(instance != null) {return instance;}
            instance = new Singleton();
            if(INSTANCE.compareAndSet(null, instance)) {return instance;}
        }
    }
}

CAS 是一种乐观锁,依赖于底层硬件的实现,绝对于锁它没有线程切换和阻塞的额定耗费,能够反对较大的并发度,然而如果忙期待始终执行不胜利,也会对 CPU 造成较大的执行开销。

2.2 饿汉(线程平安)

不同于懒加载的提早实现实例,咱们也能够在程序启动时就加载好单例对象:

public class Singleton {
    /** 保障构造方法公有,不被外界类所创立 **/
    private Singleton() {}
    /** 间接获取实例对象 **/
    private static Singleton instance = new Singleton();
    
    public static Singleton getInstance() {return instance;}
}

这样的益处是线程平安,单例对象在类加载时就曾经被初始化,当调用单例对象时只是把早曾经创立好的对象赋值给变量。毛病就是如果始终没有调用该单例对象的话,就会造成资源节约。除此之外还有其余的实现形式。

2.3 动态外部类

public class Singleton {
    /** 保障构造方法公有,不被外界类所创立 **/
    private Singleton() {}
    /** 利用动态外部类获取单例对象 **/
    private static class SingletonInstance {private static final Singleton instance = new Singleton();
    }

    public static Singleton getInstance() {return SingletonInstance.instance;}
}

动态外部类的办法联合了饿汉形式,它们都采纳了类加载机制来保障当初始化实例时只有一个线程执行,从而保障了 多线程下的平安操作。起因就是 JVM 在类初始化阶段时会创立一个锁,该锁能够保障多个线程同步执行类初始化工作。

然而动态外部类不会在程序启动时创立单例对象,它是在外界调用 getInstance办法时才会装载外部类,从而实现单例对象的初始化工作,不会造成资源节约。

然而这种办法也存在毛病,它能够通过反射来进行毁坏。上面就该提到枚举形式了

2.4 枚举

枚举是《Effective Java》作者举荐的单例实现形式,枚举只会装载一次,无论是序列化、反序列化、反射还是克隆都不会新创建对象。因而它也不会被反射所毁坏。

public class Singleton {INSTANCE;}

所以这种形式是线程平安的,而且无奈被反射而毁坏

三、单例模式的利用场景

3.1 Windows 工作管理器

在一个 windows 零碎中只有一个工作管理器,这就是一种单例模式的利用。

3.2 网站的计数器

因为计数器的作用,就必须保障计数器对象保障惟一

3.3 JDK 中的单例

3.3.1 java.lang.Runtime

Every Java application has a single instance of class Runtime that allows the application to interface with the environment in which the application is running. The current runtime can be obtained from the getRuntime method.

An application cannot create its own instance of this class.

每个 java 程序都含有惟一的 Runtime 实例,保障实例和运行环境相连接。以后运行时能够通过 getRuntime 办法取得

咱们来看看具体的代码:

public class Runtime {private static Runtime currentRuntime = new Runtime();
    
    public static Runtime getRuntime() {return currentRuntime;}

    private Runtime() {}

咱们发现这就是单例模式的饿汉加载形式。

3.3.2 java.awt.Desktop

相似的,在 java.awt.Desktop 中也存在单例模式的应用,比方:

public class Desktop {

    private DesktopPeer peer;
    
    private Desktop() {peer = Toolkit.getDefaultToolkit().createDesktopPeer(this);
    }
    // 懒加载
    public static synchronized Desktop getDesktop(){if (GraphicsEnvironment.isHeadless()) throw new HeadlessException();
        if (!Desktop.isDesktopSupported()) {
            throw new UnsupportedOperationException("Desktop API is not" +
                                                    "supported on the current platform");
        }

        sun.awt.AppContext context = sun.awt.AppContext.getAppContext();
        Desktop desktop = (Desktop)context.get(Desktop.class);

        if (desktop == null) {desktop = new Desktop();
            context.put(Desktop.class, desktop);
        }

        return desktop;
    }

这种办法就是一种提早加载的形式。

3.4 Spring Bean 作用域

比拟常见的就是 Spring Bean 作用域里的单例了,这个比拟常见,能够通过配置文件进行配置:

<bean class="..."></bean>

参考资料

https://www.zhihu.com/search?type=content&q=%E5%8D%95%E4%BE%8…

https://www.bilibili.com/video/BV1pt4y1X7kt?spm_id_from=333.3…

https://www.jianshu.com/p/137e65eb38ce

正文完
 0