Java 单例设计模式最佳实践与示例

介绍

Java 单例模式是《四人组设计模式》之一,属于创建型设计模式类别。从定义上看,它似乎是一种简单直接的设计模式,但在实现时,它涉及到许多考虑因素。

在本文中,我们将学习单例设计模式的原则,探讨实现单例设计模式的不同方法,并介绍一些最佳实践。

单例模式原则

  • 单例模式限制了类的实例化,并确保在 Java 虚拟机中只存在一个类的实例。
  • 单例类必须提供一个全局访问点来获取类的实例。
  • 单例模式用于日志记录、驱动对象、缓存和线程池
  • 单例设计模式也被用于其他设计模式,比如抽象工厂建造者原型外观等。
  • 单例设计模式还被用于核心Java类(例如,java.lang.Runtimejava.awt.Desktop)。

Java单例模式实现

为了实现单例模式,我们有不同的方法,但它们都有以下共同的概念。

  • 私有构造函数以限制其他类实例化该类。
  • 同一类的私有静态变量,是该类的唯一实例。
  • 公共静态方法返回该类的实例,这是外界获取单例类实例的全局访问点。

在接下来的部分中,我们将学习单例模式实现的不同方法以及实现中的设计考虑事项。

1. 渴望初始化

在渴望初始化中,单例类的实例在类加载时创建。渴望初始化的缺点是即使客户端应用程序可能不使用它,该方法也会被创建。以下是静态初始化单例类的实现:

package com.journaldev.singleton;

public class EagerInitializedSingleton {

    private static final EagerInitializedSingleton instance = new EagerInitializedSingleton();

    // 私有构造函数以避免客户端应用程序使用构造函数
    private EagerInitializedSingleton(){}

    public static EagerInitializedSingleton getInstance() {
        return instance;
    }
}

如果您的单例类没有使用大量资源,这是要使用的方法。但在大多数情况下,单例类是为文件系统、数据库连接等资源创建的。除非客户端调用 getInstance 方法,否则我们应该避免实例化。此外,该方法不提供任何异常处理选项。

2. 静态块初始化

静态块初始化实现类似于急切初始化,唯一的区别是类的实例是在提供了选项的静态块中创建的,用于异常处理

package com.journaldev.singleton;

public class StaticBlockSingleton {

    private static StaticBlockSingleton instance;

    private StaticBlockSingleton(){}

    // 用于异常处理的静态块初始化
    static {
        try {
            instance = new StaticBlockSingleton();
        } catch (Exception e) {
            throw new RuntimeException("Exception occurred in creating singleton instance");
        }
    }

    public static StaticBlockSingleton getInstance() {
        return instance;
    }
}

急切初始化和静态块初始化都会在实例被使用之前创建实例,这不是最佳实践。

3. 懒加载

使用懒加载方法来实现单例模式会在全局访问方法中创建实例。以下是使用此方法创建单例类的示例代码:

package com.journaldev.singleton;

public class LazyInitializedSingleton {

    private static LazyInitializedSingleton instance;

    private LazyInitializedSingleton(){}

    public static LazyInitializedSingleton getInstance() {
        if (instance == null) {
            instance = new LazyInitializedSingleton();
        }
        return instance;
    }
}

在单线程环境下,上述实现方式是有效的,但是在多线程系统中,如果多个线程同时处于if条件内部,就会出现问题。这会破坏单例模式,并且两个线程将获得单例类的不同实例。在接下来的部分中,我们将看到创建线程安全单例类的不同方法。

4. 线程安全的单例模式

A simple way to create a thread-safe singleton class is to make the global access method synchronized so that only one thread can execute this method at a time. Here is a general implementation of this approach:

package com.journaldev.singleton;

public class ThreadSafeSingleton {

    private static ThreadSafeSingleton instance;

    private ThreadSafeSingleton(){}

    public static synchronized ThreadSafeSingleton getInstance() {
        if (instance == null) {
            instance = new ThreadSafeSingleton();
        }
        return instance;
    }

}

前面的实现可以很好地提供线程安全性,但由于与同步方法相关的成本,它会降低性能,尽管我们只需要为可能创建单独实例的前几个线程使用它。为了避免每次都有这额外开销,采用了双重检查锁定原则。在这种方法中,使用同步块放在if条件内部,并额外检查以确保只创建一个单例类的实例。以下代码片段提供了双重检查锁定的实现:

public static ThreadSafeSingleton getInstanceUsingDoubleLocking() {
    if (instance == null) {
        synchronized (ThreadSafeSingleton.class) {
            if (instance == null) {
                instance = new ThreadSafeSingleton();
            }
        }
    }
    return instance;
}

继续学习,了解线程安全的单例类

5. Bill Pugh 单例模式实现

在Java 5之前,Java内存模型存在许多问题,先前的方法在某些场景中失败,即当太多线程同时尝试获取单例类的实例时。因此,Bill Pugh提出了一种不同的方法,使用内部静态辅助类创建单例类。以下是Bill Pugh单例模式的实现示例:

package com.journaldev.singleton;

public class BillPughSingleton {

    private BillPughSingleton(){}

    private static class SingletonHelper {
        private static final BillPughSingleton INSTANCE = new BillPughSingleton();
    }

    public static BillPughSingleton getInstance() {
        return SingletonHelper.INSTANCE;
    }
}

请注意私有的内部静态类,其中包含单例类的实例。当单例类被加载时,SingletonHelper类不会加载到内存中,只有当有人调用getInstance()方法时,该类才会加载并创建单例类实例。这是单例类最广泛使用的方法,因为它不需要同步。

6. 使用反射破坏单例模式

反射可以用来破坏先前的所有单例实现方法。以下是一个示例类:

package com.journaldev.singleton;

import java.lang.reflect.Constructor;

public class ReflectionSingletonTest {

    public static void main(String[] args) {
        EagerInitializedSingleton instanceOne = EagerInitializedSingleton.getInstance();
        EagerInitializedSingleton instanceTwo = null;
        try {
            Constructor[] constructors = EagerInitializedSingleton.class.getDeclaredConstructors();
            for (Constructor constructor : constructors) {
                // 这段代码将破坏单例模式
                constructor.setAccessible(true);
                instanceTwo = (EagerInitializedSingleton) constructor.newInstance();
                break;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(instanceOne.hashCode());
        System.out.println(instanceTwo.hashCode());
    }

}

当您运行前面的测试类时,您会注意到两个实例的hashCode不相同,这破坏了单例模式。反射非常强大,被广泛应用于许多框架中,如Spring和Hibernate。继续学习,请查看Java反射教程

7. 枚举单例

为了克服这种情况,Joshua Bloch建议使用enum来实现单例设计模式,因为Java确保任何enum值在Java程序中仅实例化一次。由于Java Enum值是全局可访问的,所以单例也是如此。缺点是enum类型有些不灵活(例如,它不允许延迟初始化)。

package com.journaldev.singleton;

public enum EnumSingleton {

    INSTANCE;

    public static void doSomething() {
        // 做一些事情
    }
}

8. 序列化和单例

有时在分布式系统中,我们需要在单例类中实现Serializable接口,以便可以将其状态存储在文件系统中,并在以后的某个时间点检索它。这是一个实现了Serializable接口的小型单例类:

package com.journaldev.singleton;

import java.io.Serializable;

public class SerializedSingleton implements Serializable {

    private static final long serialVersionUID = -7604766932017737115L;

    private SerializedSingleton(){}

    private static class SingletonHelper {
        private static final SerializedSingleton instance = new SerializedSingleton();
    }

    public static SerializedSingleton getInstance() {
        return SingletonHelper.instance;
    }

}

序列化单例类的问题在于,每当我们对其进行反序列化时,它都会创建类的新实例。这是一个例子:

package com.journaldev.singleton;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;

public class SingletonSerializedTest {

    public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
        SerializedSingleton instanceOne = SerializedSingleton.getInstance();
        ObjectOutput out = new ObjectOutputStream(new FileOutputStream(
                "filename.ser"));
        out.writeObject(instanceOne);
        out.close();

        // 从文件反序列化为对象
        ObjectInput in = new ObjectInputStream(new FileInputStream(
                "filename.ser"));
        SerializedSingleton instanceTwo = (SerializedSingleton) in.readObject();
        in.close();

        System.out.println("instanceOne hashCode="+instanceOne.hashCode());
        System.out.println("instanceTwo hashCode="+instanceTwo.hashCode());

    }

}

该代码产生以下输出:

Output
instanceOne hashCode=2011117821 instanceTwo hashCode=109647522

因此它破坏了单例模式。要克服这种情况,我们只需要提供readResolve()方法的实现。

protected Object readResolve() {
    return getInstance();
}

之后,您将注意到测试程序中两个实例的hashCode是相同的。

阅读关于Java序列化Java反序列化的内容。

结论

本文介绍了单例设计模式。

通过更多的Java教程继续学习。

Source:
https://www.digitalocean.com/community/tutorials/java-singleton-design-pattern-best-practices-examples