Java设计模式-单例模式最佳实践-LMLPHP

1. 单例模式简介

Java 单例模式是四大设计模式之一,属于创建型设计模式。从定义上看,它似乎是一种简单的设计模式,但在实现时,如若不注意,它会带来很多问题。

在本文中,我们将了解单例设计模式原则,探索实现单例设计模式的不同方法,以及一些最佳实践。

2. 单例模式原理

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

3. 单例模式实现方式

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

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

Java设计模式-单例模式最佳实践-LMLPHP

3.1 急切初始化

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

public class EagerInitializedSingleton {

    private static final EagerInitializedSingleton instance = new EagerInitializedSingleton();

    // private constructor to avoid client applications using the constructor
    private EagerInitializedSingleton(){}

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

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

3.2 静态块初始化

静态块初始化实现与急切初始化类似,不同之处在于类的实例是在静态块中创建的,并提供了异常处理的选项。

public class StaticBlockSingleton {

    private static StaticBlockSingleton instance;

    private StaticBlockSingleton(){}

    // static block initialization for exception handling
    static {
        try {
            instance = new StaticBlockSingleton();
        } catch (Exception e) {
            throw new RuntimeException("Exception occurred in creating singleton instance");
        }
    }

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

注意:立即初始化和静态块初始化都会在使用之前创建实例,这不是最佳做法

3.3 延迟初始化

实现单例模式的惰性初始化方法在全局访问方法中创建实例。以下是使用此方法创建单例类的示例代码:

public class LazyInitializedSingleton {

    private static LazyInitializedSingleton instance;

    private LazyInitializedSingleton(){}

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

上述实现在单线程环境中运行良好,但在多线程系统中,如果if同时有多个线程处于条件中,则可能会导致问题。它将破坏单例模式,并且两个线程将获得单例类的不同实例。在下一节中,我们将看到创建线程安全的单例类的不同方法。

3.4 线程安全单例

Java设计模式-单例模式最佳实践-LMLPHP
创建线程安全的单例类的一个简单方法是将全局访问方法同步,以便一次只有一个线程可以执行此方法。以下是此方法的一般实现:

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;
}

以上代码的同步锁,仅仅加到了if (instance == null) {这个判断下面,也就是只有当实例未初始化时,才到这里面同步去初始化,也就是并发只有在未初始化那一短暂时刻才会发生,极大降低了同步锁带来的并发开销!视为最佳方式!

3.5 Bill Pugh 单例实现

在 Java 5 之前,Java 内存模型存在很多问题以前的方法在某些情况下会失败,因为太多线程同时尝试获取单例类的实例。因此,Bill Pugh提出了一种不同的方法,使用内部静态辅助类来创建单例类。以下是 Bill Pugh 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()方法时,才会加载该类并创建单例类实例。这是单例类最广泛使用的方法,因为它不需要同步。

3.6 使用反射来破坏单例模式

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

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) {
                // This code will destroy the singleton pattern
                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 反射教程。

3.7 枚举单例

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

public enum EnumSingleton {

    INSTANCE;

    public static void doSomething() {
        // do something
    }
}

3.8 序列化和单例

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

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;
    }
}

序列化单例类的问题在于,每当我们反序列化它时,它都会创建该类的新实例。以下是一个例子:

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();

        // deserialize from file to object
        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测试程序中的两个实例是相同的

4. 总结

好啦,以上就是关于java设计模式的单例模式详细介绍,整理出八种初始化方式及其利弊分析,最佳方式推荐是3.4章节的线程安全单例

08-13 07:16