Java 单例实现解析

什么时候使用Singleton

Singleton指仅仅被实例化一次的类。Singleton通常用来代表那些本质上唯一的系统组件,比如文件系统,窗口管理器,日历等。Singleton的类会使客户端测试变得异常困难,因为无法给Singleton替换模拟实现,除非Singleton实现一个充当其类型的接口。


Java类的实例化

按照是否调用类的构造器,可以简单的将类实例化的方法分为两大类:通过构造器实例化和不通过构造器实例化。下面以实例化 Windows10FileSystem 类为例进行详细说明。

Windows10FileSystem.java

public class Windows10FileSystem {
    private String name;
    private String description;
    // use default constructor
    // getter/setter method
}

这个文件系统十分的“简陋”,只包括文件系统名称和文件系统描述。

通过构造器实例化类

  • 通过 new 关键字。

    Windows10FileSystem fileSystem = new Windows10FileSystem();
    
  • 通过 ClassnewInstance() 方法。

    Windows10FileSystem fileSystem = (Windows10FileSystem) Class.forName("Windows10FileSystem").newInstance();
    

    注意:forName的参数必须是类的全限定名,这里为了简单使用类名。

  • 通过 ConstructornewInstance() 方法。

    Constructor constructor = Windows10FileSystem.class.getConstructor();
    Windows10FileSystem fileSystem = constructor.newInstance();
    

    上述方法只适用于构造器可被访问的场景,如果构造器为private,可以使用下面的方法访问构造器。

    如果 Windows10FileSystem的构造器是私有(private)的,借助 AccessibleObject.setAccessible(true) 可以调用私有的构造器:

    Constructor constructor = Windows10FileSystem.class.getDeclaredConstructor();
    constructor.setAccessible(true);
    Windows10FileSystem fileSystem = constructor.newInstance();
    

不使用构造器实例化类

  • 通过 clone() 函数。

    首先Windows10FileSystem 类需要实现clone()方法:

    @Override
    public Windows10FileSystem clone() {
        Windows10FileSystem copyFileSystem = new Windows10FileSystem();
        copyFileSystem.setDescription(this.description);
        copyFileSystem.setName(this.name);
        return copyFileSystem;
    }
    
    Windows10FileSystem fileSystem = new Windows10FileSystem();
    fileSystem.setName("clone");
    fileSystem.setDescription("graphical operating system");
    
    Windows10FileSystem cloneFileSystem = fileSystem.clone();
    
  • 通过反序列化。

    假设保存对象的文件为:fileSystem.obj

    ObjectInputStream in = new ObjectInputStream(new FileInputStream("fileSystem.obj"));
    Windows10FileSystem fileSystem = (Windows10FileSystem) in.readObject();
    

Singleton的实现

实现Singleton的思路

上一节我们已经了解Java中实例化一个类的多种方法,而Singleton的目标就是要确保类仅仅只被实例化一次,为此我们需要控制类实例化的入口,或者控制入口方法的调用次数或者控制方法每次调用返回同一个对象,确保一个类只被实例化一次。

  • 构造器私有化,降低类构造器的可见范围。

    private Windows10FileSystem() {}
    
  • 构造器私有化虽然可以降低访问范围,但享有特权的客户端可以借助 AccessibleObject.setAccessible(true) 方法,通过反射机制调用私有的构造器,为了抵御这种攻击,需要修改私有构造器,在构造器第二次调用时抛出异常,阻止类被多次实例化。

    private static AtomicBoolean FIRST_INSTANTIATION = new AtomicBoolean(true);
    
    private Windows10FileSystem() {
        if (FIRST_INSTANTIATION.get()) {
            FIRST_INSTANTIATION.compareAndSet(true,false);
        } else {
            throw  new UnsupportedOperationException();
        }
    }
    

    注意:这里没有考虑并发调用构造器的问题,在Java中可以使用锁或synchronized简单的进行方法同步

  • 为了让Singleton支持序列化,只实现Serializable 接口是不够的,为了维护并保证Singleton,所有实例域都必须是瞬时(transient)的,并提供一个readResolve() 方法,该方法每次都返回同一个实例。

    private static Windows10FileSystem INSTANCE = new Windows10FileSystem();
    
    private Object readResolve() {
        return INSTANCE;
    }
    

    该方法防止攻击的原理和简单的模拟可以参考《Effective Java》中的第77条-对于实例控制,枚举类型优先于readResolve

  • 类不要实现 Cloneable接口和clone()方法。

    保证不可以调用对象的clone() 函数来实例化。

实现Singleton的三种方法

有多种方法可以实现Singleton,虽然每种方法的具体细节不一样,但是每种方法的目标都是相同的:确保类只被实例化一次。强烈推荐下文描述的第一种方法来实现Singleton:使用单元素枚举类型实现Singleton。

单元素枚举类型实现Singleton

Java从1.5发型版本开始支持通过枚举(enum)实现Singleton,下面以enum 实现一个Windows 10文件系统,这个文件系统非常的简陋,只提供了名字和描述两项基本信息,具体的代码如下:

public enum Windows10FileSystem {
    /**
     * singleton file system instance
     */
    INSTANCE("Windows 10", "graphical operating system");

    private String name;
    private String description;

    Windows10FileSystem(String name, String separator) {
        this.name = name;
        this.description = separator;
    }

    public String getBaseInfo() {
        return name + "\t" + description;
    }
}

使用enum 实现Singleton更加简洁,无偿提供了序列化机制,绝对防止多次实例化,即使是在面对复杂的序列化和反射攻击时,这种方法依然绝对可靠。强烈推荐使用该方法实现Singleton。

导出公有静态成员(final 域)实现Singleton
public class Windows10FileSystem implements Serializable {
    private static final AtomicBoolean FIRST_INSTANTIATION = new AtomicBoolean(true);
    public static final Windows10FileSystem INSTANCE = new Windows10FileSystem("Windows 10","graphical operating system");

    transient private String name;
    transient private String description;

    /**
     * 构造器私有化,并防止多次调用
     * @param name 文件系统名称
     * @param separator 文件系统描述
     */
    private Windows10FileSystem(String name, String separator) {
        if (FIRST_INSTANTIATION.get()) {
            FIRST_INSTANTIATION.compareAndSet(true,false);
            this.name = name;
            this.description = separator;
        } else {
            throw new UnsupportedOperationException("windows file system can only be instantiated once");
        }
    }

    /**
     * 防止反系列化攻击
     * @return file system object
     */
    private Object readResolve() {
        return INSTANCE;
    }

    public String getBaseInfo() {
        return name + "\t" + description;
    }
}

static变量的初始化顺序参考JLS 8.7

公有静态工厂方法实现Singleton

这种方法相比与导出公有静态成员(final 域)实现Singleton而言只是公有静态变量变成了一个工厂方法,每次调用工厂方法都返回同一个实例。

private static final Windows10FileSystem INSTANCE = new Windows10FileSystem("Windows 10","graphical operating system");
public static Windows10FileSystem getInstance() {
        return INSTANCE;
    }

两种实现Singleton方法的核心都是通过私有化构造器来控制类的实例化。公有域方法的主要优势在于,类的成员声明很清楚的表明这个类是一个Singleton(可读性强):公有的静态域是final的,所以该域将总是包含相同的对象。公有域在性能上已经不再拥有任何优势,现代化的JVM实现几乎都能将静态工厂方法的调用内联化。静态工厂方法的优势在于,它提供了更高的灵活性:在不改变API的条件下,我们可以改变该类是否是Singleton的想法。工厂方法返回该类的唯一实例,但是,这可以很容易的被修改,比如修改为每一个线程返回同一个实例。

你可能感兴趣的:(Java 单例实现解析)