前言

单例模式创建型模式的一种,也是设计模式中最为简单的一种。顾名思义,单例模式指的是类的实例化对象只允许有一个存在。单例模式广泛的应用于众多只需要一个实例化对象存在的场景,如常见的配置文件信息的读取、计算机中许多系统资源的管理等。

单例模式的一个直接目的就是获取类的唯一一个实例化对象,那么为什么要使用单例模式来进行获取,而不是直接通过new指令来随需随建呢?之前所写的简单程序中,对象的获取都是使用类似类名 类变量名 = new 类名()的代码进行实例化。这样做用户只需要调用类的构造方法,实际上对象的创建是交给JVM进行处理,JVM会在堆内存中进行空间的分配、对象的构建以及将对象的地址保存在栈内存的变量中进行存放。这一系列的工作实际上开销很大,如果类对象随需随建,那么程序的性能自然不会高,而且很多时候我们也不需要随需随建。

因此,单例模式通过隐藏构造方法并提供全局的对象获取方法来保证在内存中始终只有一个对象,从而避免了频繁的创建和销毁对象,节省了内存空间,提高了程序的性能。

1. 饿汉式

饿汉式.png

1.1 基于静态常量

饿汉式的第一种实现是基于静态常量的实现方式。它通过将构造方法使用private进行私有化,同时在类内部进行实例化并使用static关键字修饰实例,最后提供一个全局的静态方法供用户进行实例的获取。

  1. class Singleton{
  2. private final static Singleton instance = new Singleton();
  3. private Singleton(){}
  4. public static Singleton getInstance(){
  5. return instance;
  6. }
  7. }
  8. public class SingletonDemo {
  9. public static void main(String[] args) {
  10. Singleton instance1 = Singleton.getInstance();
  11. Singleton instance2 = Singleton.getInstance();
  12. System.out.println(instance1 == instance2);
  13. System.out.println("instance1 = " + instance1);
  14. System.out.println("instance2 = " + instance2);
  15. }
  16. }

此时程序的输出为:

  1. true
  2. instance1 = Singleton.Singleton@1d81eb93
  3. instance2 = Singleton.Singleton@1d81eb93

从输出结果中可以看出,通过两次调用getInstance()获取到的两个实例化对象是一样的。因为,此时类Singleton只创建一个实例,并且使用final和static关键字进行修饰。

这种方式是我们能想到的最简单且直观的一种,它在类装载的时候就完成了实例化工作,同时借助ClassLoader的内部机制实现了线程安全。不足之处在于,实例在类加载时一起进行创建,此后该实例会一直保存在内存空间中。如果后续没有使用到类对象,那么不免会造成内存的浪费。而且,类装载时直接完成实例化工作,没有实现所谓的懒加载(Lazy Loading)。

1.2 基于静态代码块

饿汉式的另一种实现是基于静态代码块的方式,即将类对象实例化的工作放在了类内部的静态代码块中执行。这种方式效果上和前一种是一致的,静态代码块会在类加载的时候执行,同样没有实现懒加载的效果,以及可能会造成内存的浪费。

  1. class Singleton{
  2. private final static Singleton instance;
  3. static {
  4. instance = new Singleton();
  5. }
  6. private Singleton(){}
  7. public static Singleton getInstance(){
  8. return instance;
  9. }
  10. }
  11. public class SingletonDemo {
  12. public static void main(String[] args) {
  13. Singleton instance1 = Singleton.getInstance();
  14. Singleton instance2 = Singleton.getInstance();
  15. System.out.println(instance1 == instance2);
  16. System.out.println("instance1 = " + instance1);
  17. System.out.println("instance2 = " + instance2);
  18. }
  19. }

总结:两种饿汉式的实现方式没有实现懒加载,同时可能会有内存浪费问题,但它是线程安全的。

2. 懒汉式

懒汉式.png

2.1 线程不安全实现

前面饿汉式的实现方式中实例会在类加载的过程中创建,不管后续有没有代码调用getInstance()来获取实例,实例会一直保存在内存空间中。而懒汉式中实例不会随着类加载而创建,而是在调用getInstance()时才进行实例化,同时需要进行判空操作,避免重复实例化。

  1. class Singleton{
  2. private static Singleton instance;
  3. private Singleton(){}
  4. public static Singleton getInstance(){
  5. if (instance == null){
  6. instance = new Singleton();
  7. }
  8. return instance;
  9. }
  10. }
  11. public class SingletonDemo {
  12. public static void main(String[] args) {
  13. Singleton instance1 = Singleton.getInstance();
  14. Singleton instance2 = Singleton.getInstance();
  15. System.out.println(instance1 == instance2);
  16. System.out.println("instance1 = " + instance1);
  17. System.out.println("instance2 = " + instance2);
  18. }
  19. }

只有代码在调用getInstance()来获取实例时,实例才被创建,从而实现了懒加载,也就不会有内存空间的浪费。但这种方式有其他的问题吗?注意这里实例化的操作不是随类加载完成的,自然就无法借助ClassLoader来自动实现同步,避免线程安全问题的出现。而在不同的线程在调用getInstance()时,可能有多个线程都通过了if (instance == null)的判空操作,它们都认为此时内存空间中不存在可用实例,从而进行实例的构建。每个线程在构建实例时是不知道其他线程的动作的,因此,此时就出现了线程不安全问题。

2.2 同步方法实现

前一种懒汉式方法由于不会自动实现线程同步,且程序中没有显式的添加线程同步逻辑,故程序会出现线程不安全问题。因此,要想实现懒加载又要保证线程同步就需要使用同步机制。

浅析Java中的多线程

常用的同步机制有synchronized关键字和Lock锁加条件变量两种方法,其中synchronized又有同步方法和同步代码块两种实现机制。如果使用synchronized来实现线程同步,那么只需要将getInstance()使用synchronized关键字进行修饰,如下所示:

  1. class Singleton{
  2. private static Singleton instance;
  3. private Singleton(){
  4. }
  5. public static synchronized Singleton getInstance(){
  6. if (instance == null){
  7. instance = new Singleton();
  8. }
  9. return instance;
  10. }
  11. }
  12. public class SingletonDemo {
  13. public static void main(String[] args) {
  14. Singleton instance1 = Singleton.getInstance();
  15. Singleton instance2 = Singleton.getInstance();
  16. System.out.println(instance1 == instance2);
  17. System.out.println("instance1 = " + instance1);
  18. System.out.println("instance2 = " + instance2);
  19. }
  20. }

使用synchronized同步方法保证了线程安全,但是使用synchronized进行方法同步的开销是很大的,详情可自行查阅Java并发编程的相关内容。因此,不管此时有多少线程,不管此时内存中是否有可用实例,每个线程调用getInstance()时都需要进行同步,显然这样的方式效率太低了,并不是我们想要的。

2.3 同步代码块实现

使用synchronized实现同步的另一种方式是用其同步可能会出现同步问题的代码块,实现方式较为简单,这里就不再赘述。

  1. class Singleton{
  2. private static Singleton instance;
  3. private Singleton(){
  4. }
  5. public static Singleton getInstance(){
  6. if (instance == null){
  7. synchronized(Singleton.class){
  8. instance = new Singleton();
  9. }
  10. }
  11. return instance;
  12. }
  13. }
  14. public class SingletonDemo{
  15. public static void main(String[] args) {
  16. Singleton instance1 = Singleton.getInstance();
  17. Singleton instance2 = Singleton.getInstance();
  18. System.out.println(instance1 == instance2);
  19. System.out.println("instance1 = " + instance1);
  20. System.out.println("instance2 = " + instance2);
  21. }
  22. }

但它同样会出现多个线程都通过if(instance == null)的判空操作,然后进行构建多个实例的情况。因此,这样的方式是无法保证线程同步。

3. 双检锁/双重校验锁

前面使用synchronized同步代码块的方式无法保证线程同步,原因是因为可能有多个线程通过判空操作,从而在内存中会创建多个实例。为了解决这个问题,我们可以在synchronized同步代码块中再进行一次判断。这样即使线程通过了第一次的判空操作,还需要再次进行判断,而此时只能有一个线程可以创建实例,从而实现了线程同步。

  1. class Singleton{
  2. private volatile static Singleton instance;
  3. private Singleton(){}
  4. public static synchronized Singleton getInstance(){
  5. if (instance == null){
  6. synchronized (Singleton.class){
  7. if (instance == null){
  8. instance = new Singleton();
  9. }
  10. }
  11. }
  12. return instance;
  13. }
  14. }
  15. public class SingletonDemo {
  16. public static void main(String[] args) {
  17. Singleton instance1 = Singleton.getInstance();
  18. Singleton instance2 = Singleton.getInstance();
  19. System.out.println(instance1 == instance2);
  20. System.out.println("instance1 = " + instance1);
  21. System.out.println("instance2 = " + instance2);
  22. }
  23. }

通过双重锁机制不仅保证了线程同步,同时还实现了懒加载和避免了内存空间可能的浪费,因此它是一种优秀的单例模式的实现方式,推荐使用。

4. 静态内部类实现

如果自己不想实现线程同步,又要实现懒加载,静态内部类是一种更好的实现途径。静态内部类不会随外部类的加载而加载,而是在使用它时才会加载。因此,如果将实例化操作通过内部类实现,而且实例化又是在getInstance()内部实现,这样既通过ClassLoader保证了线程同步,又借助静态内部类实现了懒加载。

浅析Java中的内部类

  1. class Singleton{
  2. private static Singleton instance;
  3. private Singleton(){}
  4. private static class SingletonInstance{
  5. private static final Singleton INSTANCE = new Singleton();
  6. }
  7. public static Singleton getInstance(){
  8. return SingletonInstance.INSTANCE;
  9. }
  10. }
  11. public class SingleDemo {
  12. public static void main(String[] args) {
  13. Singleton instance1 = Singleton.getInstance();
  14. Singleton instance2 = Singleton.getInstance();
  15. System.out.println(instance1 == instance2);
  16. System.out.println("instance1 = " + instance1);
  17. System.out.println("instance2 = " + instance2);
  18. }
  19. }

5. 枚举实现

枚举实例的创建默认就是线程安全的,并且任何时候都是单例。因此,使用枚举是一种更为优秀的实现方式。

  • 枚举类隐藏了私有的构造器。
  • 枚举类的域 是相应类型的一个实例对象
  1. enum Singleton {
  2. INSTANCE;
  3. public static Singleton getInstance() {
  4. return Singleton.INSTANCE;
  5. }
  6. }
  7. public class SingletonDemo {
  8. public static void main(String[] args) {
  9. Singleton instance1 = Singleton.getInstance();
  10. Singleton instance2 = Singleton.getInstance();
  11. System.out.println(instance1 == instance2);
  12. System.out.println("instance1 = " + instance1);
  13. System.out.println("instance2 = " + instance2);
  14. }
  15. }

6.总结

综上所述,单例模式在实际使用中推荐使用双重锁、静态内部类和枚举三种实现方式。它们不仅实现了线程同步,而且通过懒加载避免了内存空间的浪费,提高了程序的性能和执行效率。单例模式适合的场景有:

  • 需要生成唯一序列的环境
  • 需要频繁实例化然后销毁实例的场景
  • 创建对象开销较大但又需要经常用到对象的场景
  • 方便资源相互通信的场景

7. 参考

设计模式之单例模式 从未这么明白的设计模式(一):单例模式 菜鸟教程-单例模式