单例模式

单例模式

案例

我们要开发一款电脑端的应用出来,它可以安装在手机上,然后进行使用。下面我们写出一些简单的代码:

1.首先是这款应用:

/**
 * 模拟开发了一款应用程序
 */
public class App {
    public void open() {
        System.out.println("软件打开中~~");
    }
}

2.模拟使用应用:

/**
 * 模拟客户端使用软件
 */
public class Main {
    public static void main(String[] args) {
        App app = new App();
        app.open();
    }
}

上面的代码都比较简单,只是为了说明一个问题。那就是在我们的电脑使用时,通常来说一款应用只会有一个,打开了之后,后面再打开,也同样应该是同一个应用,不应该时我们通过图标多次双击后就打开了多个软件。

像这样我们无须创建多个实例的情况,在设计模式中就称为单例设计模式。

模式介绍

单例模式,属于创建类型的一种常用的软件设计模式。通过单例模式的方法创建的类在当前进程中只有一个实例(根据需要,也有可能一个线程中属于单例,如:仅线程上下文内使用同一个实例)。

特点:

单例模式的主要特点有三个:

  • 某个类只能有一个实例。
  • 它必须自行创建这个实例。
  • 它必须自行向整个系统提供这个实例。

从具体实现角度来说,就是以下三点:

  • 单例模式的类只提供私有的构造函数。
  • 类定义中含有一个该类的静态私有对象。
  • 该类提供了一个静态的公有的函数用于创建或获取它本身的静态私有对象。

从介绍上来说单例模式就是这么的简单,接下来我们按照上面介绍的要点来对案例进行改造。

代码改造

改造后的应用类:

饿汉式

/**
 * 饿汉式
 */
public class App {
    // 静态私有对象
    private static App app = new App();

    // 私有的构造函数
    private App() {
    }

    // 静态的公有的函数用于创建或获取它本身的静态私有对象
    public static App getInstance() {
        return app;
    }

    public void open() {
        System.out.println("软件打开中~~");
    }
}

客户端使用:

public class Main {
    public static void main(String[] args) {
        App app1 = App.getInstance();
        app1.open();
        App app2 = App.getInstance();
        System.out.println(app1 == app2);
    }
}

经过我们的改造后,我们每次去获取App类的实例时都是同一个,这样就保证了唯一性。

其实上面的实现方式称之为饿汉式,因为它不管你有没有使用App类对象,只要App类被加载了,就会导致类的初始化,而在类初始化阶段,会对静态变量app赋值,然后对象就会创建,所以称为饿汉式。而单例模式还有其他几种方式。

懒汉式:

/**
 * 懒汉式
 */
public class Lazy {
    private static Lazy instance;

    private Lazy() {
    }

    public static Lazy getInstance() {
        // 在第一次使用时再创建实例
        if (instance == null) {
            instance = new Lazy();
        }
        return instance;
    }
}

可以看到它与饿汉式的区别就是,它在初次使用的时候对象才会真正的创建。这样的好处就是它不会在没有使用它的时候造成资源的浪费。

线程安全懒汉式:

/**
 * 线程安全懒汉式
 */
public class ThreadSafeLazy {
    private static ThreadSafeLazy instance;

    private ThreadSafeLazy() {
    }

    // 在第一次使用时再创建实例,同时使用 synchronized 关键字保证线程安全
    public static synchronized ThreadSafeLazy getInstance() {
        if (instance == null) {
            instance = new ThreadSafeLazy();
        }
        return instance;
    }

}

我们在代码中通过synchronized关键字实现了线程安全,但是由于syncronized关键字具有排他性,它使得多个线程访问时,只能有一个线程访问getInstance方法,性能低下。

双重检查懒汉式:

/**
 * 双重检查懒汉式
 */
public class DoubleCheckLazy {
    // 加入 volatile 关键字保证可见性/防止指令重排
    private static volatile DoubleCheckLazy instance;

    private DoubleCheckLazy() {
    }

    public static DoubleCheckLazy getInstance() {
        // 第一个判断:是使得在实例已经初始化了之后,不用继续访问判断里面的内容,直接跳到判断之外 return instance
        if (instance == null) {
            // 通过在代码块中加锁,使得更多的线程能够调用 getInstance() 方法,同时达到线程安全的作用
            synchronized (DoubleCheckLazy.class) {
                // 第二个判断:这个判断是为了保证只创建一次实例
                if (instance == null) {
                    instance = new DoubleCheckLazy();
                }
            }
        }
        return instance;
    }
}

双重检查,就像它的名字一样,在代码中有两次判断,这两次判断的作用也是不同的。

  • 第二个判断:在同步代码块里面的判断是为了保证对象还没有实例化也就是instance == null的时候,如果有多个线程同时访问了getInstance方法,这时它们就都会通过第一个判断。但是由于synchronized关键字的存在,多个线程被阻隔在外面,只有一个线程会进入同步块中。由于是第一线程,这个时候我们的instance对象还是为null,然后它就会实例化对象。其余的线程等到它执行完并释放锁后,通过竞争后,又有一个线程(第二个线程)进入同步块中,但是这个时候由于第一个线程已经实例化了instance对象,同时它被volatile关键字修饰(保证了可见性,防止指令重排),使得第二个线程能够读到instance对象已经被实例化,那么它在判断时就不会进入判断里面,直接就会返回,保证了instance对象被实例化一次。
  • 第一个判断:在同步代码块外面的判断是为了使得下次,如果访问了getInstance()方法,由于instance对象已经实例化过了,那么不会进入判断里面,则会直接返回实例,提高了访问效率。

静态内部类式:

/**
 * 静态内部类式
 */
public class InternalClass {

    private static class StaticInternalClass {
        // 在 Internal 类被加载的时候,才会创建 instance 对象实例。
        // 因为静态属性只会被初始化一次,这就保证了 instance 对象的唯一性
        private static InternalClass instance = new InternalClass();
    }

    private InternalClass() {
    }

    public static InternalClass getInstance() {
        return StaticInternalClass.instance;
    }

}

静态内部类的方式实际上是利用了类的加载特点,由于静态属性只会被初始化一次从而保证了对象的唯一性。

枚举式:

/**
 * 枚举式单例
 */
public enum EnumSingleton {
    INSTANCE;
}

这里同样是利用Java枚举类自身的特点实现的单例。枚举方式不允许被继承,同时是线程安全,并且只能被实例化一次

模式应用

开发中使用单例模式还是比较常用的,这里举个 JDK 自带的例子Runtime类,它在java.lang包下,同时它是从 JDK1.0 就存在了的,可以说式比较经典的类。下面是它的部分源码:

/**
 * Every Java application has a single instance of class
 * <code>Runtime</code> that allows the application to interface with
 * the environment in which the application is running. The current
 * runtime can be obtained from the <code>getRuntime</code> method.
 * <p>
 * An application cannot create its own instance of this class.
 *
 * @author  unascribed
 * @see     java.lang.Runtime#getRuntime()
 * @since   JDK1.0
 */

public class Runtime {
    
	// 静态私有对象
    private static Runtime currentRuntime = new Runtime();

    // 静态的公有的函数用于获取它本身的静态私有对象
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    // 私有的构造函数
    private Runtime() {}
    
    // 省略其他源码
}

我们从中可以看出内部满足了单例设计模式的必要特点:

  • 静态私有对象
  • 私有构造函数
  • 提供了静态的公有的函数用于获取它本身的静态私有对象

总结

1.主要优点

  • 单例模式提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。
  • 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。
  • 允许可变数目的实例。基于单例模式我们可以进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例,既节省系统资源,又解决了单例单例对象共享过多有损性能的问题。

2.主要缺点

  • 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
  • 单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。
  • 现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失。

3.应用场景

  • 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器或资源管理器,或者需要考虑资源消耗太大而只允许创建一个对象。
  • 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。

参考资料

本篇文章github代码地址:https://github.com/Phoegel/design-pattern/tree/main/singleton
转载请说明出处,本篇博客地址:https://www.cnblogs.com/phoegel/p/13898834.html

原文地址:https://www.cnblogs.com/phoegel/p/13898834.html