Java单例(Singleton)

【译自:http://www.journaldev.com/1377/java-singleton-design-pattern-best-practices-with-examples

单例是设计模式中提到的模式之一,它属于创建模式的类别。从它的定义来看,这是一种非常简单的模式,但是当具体实现时,会发现它有很多需要留意的方面。关于单例的实现方法在开发人员中已经产生过很多讨论和争议。这里我们会学到关于单例的的一些准则,不同的实现方法和一些使用上的最佳实践。

单例模式

单例模式限制了类的实例化,确保只有一个类的实例存在于Java虚拟机中。这个单例类必须提供一个全局的访问点去得到这个类的唯一实例。单例通常用在例如日志、驱动、缓存和线程池等地方;另外它也可以用在其他的一些设计模式中,例如 Abstract Factory、 Builder、 PrototypeFacade 等。在核心Java类中,有很多单例的类,像 java.lang.Runtime,java.awt.Desktop等。

Java的单例模式

有很多实现单例模式的方法,但是他们都有一些共同的、需要关注的概念:

  • 私有的构造函数:限制其他类对它的实例化
  • 私有的静态变量:用于持有唯一的当前类的实例
  • 公开的静态方法 :用于返回当前类的唯一实现,这个方法也是提供给外部的全局访问点

接下来的部分,我们会学到几种不同的实现单例的方式和相应的需要关注的要求:

  1. 静态变量初始化
  2. 静态初始化块
  3. 延迟初始化
  4. 线程安全的单例
  5. Helper类实现
  6. 反射破坏初始化
  7. 枚举单例
  8. 系统化和单例

静态变量初始化

这种方式下,单例的实例在类被加载的时候就被初始化,这也是创建单例的最简单的方式。缺点就是即使这个实例没有被用到,也被创建了。

下面是这种实现方式的代码示例:

package com.journaldev.singleton;

public class EagerInitializedSingleton {
    
    private static final EagerInitializedSingleton instance = new EagerInitializedSingleton();
    
    //private constructor to avoid client applications to use constructor
    private EagerInitializedSingleton(){}

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

如果你的单例没有占用多少资源,那么可以选择这种实现方式。然后大多数情况下,单例都是针对资源而创建的,例如文件系统,数据库连接等等,所以我们应该避免对它的实例化,除非调用端调用getInstance()。方法;另外这种实现方式没有提供任务异常处理逻辑。 

静态初始化块

静态初始化块的实现方式类似于上面的静态变量初始化,除了一点:静态初始化块可以提供异常处理:

package com.journaldev.singleton;

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 occured in creating singleton instance");
        }
    }
    
    public static StaticBlockSingleton getInstance(){
        return instance;
    }
}

静态变量和静态块都是在变量在使用之前就被创建了,因此不是一个好的最佳实践方式。在接下来的片段里,我们会学到如果延迟创建单例的实例。

延迟初始化

延迟初始化是通过使用一个全局访问的方法去创建单例的实例。例如下面的代码:

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

上面的代码在单线程的情况下工作的很好,不过在多线程环境中,如果有多个有线程同时访问,则可能存在问题,单例模式可能被破坏,不同的线程将得到不同的单例的实现。接下来我们看一种线程安全的实现方式。

线程安全的单例

最简单的创建线程安全的单例就是使得全局访问方法是同步的,这样一次只有一个线程可以执行这个方法。下面代码展示了这一点:

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

上面的实现方式工作的很好,并且提供了线程安全的保障,但是它降低了性能,因为同步上的开销,同步应该只需要在一开始创建实例的时候用到。双重锁定可以用来避免每次访问的这些额外的开销,在这种实现方式下,同步块被包含在一个附加的检测中,以保证只有一个实例被创建,以下是代码实现:

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

ReadThread Safe Singleton Class

BillPugh单例实现方式

在Java5之前,Java内存模型有很多问题,使得上面的双重锁定实现方式在某些情况下,例如很多的线程同时试图去访问单例实例,会失效。因此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;
    }
}

注意:私有的、内部的、静态的类包含了单例的实例。当单例的类被加载时,这个内部的私有的类并没有被加载,只有当调用getInstance()方法的时候,这个类才会加载,然后创建单例的实例。这种实现方式被广泛的应用,并且不要求同步。我在很多工程都都使用这种方法,它很容易被理解。

ReadJava Nested Classes

反射破坏单例

反射能够破坏以上所有的实现方式:

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) {
                //Below 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 Reflection Tutorial.

枚举单例

为了克服反射的问题,Joshua Bloch建议使用枚举去实现单例模式,因为Java确保枚举值只会被初始化一次,因为Java枚举是全局访问的,因此也是一个单例。不过缺点就是枚举不够灵活,并且不允许延迟加载。

package com.journaldev.singleton;

public enum EnumSingleton {

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

ReadJava Enum

系列化和单例

有时在分布式的系统中,需要在单例上实现系列化,这样就可以把它的状态存储在文件系统上,然后在稍后的某个点将它取回,例如:

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();
        
        //deserailize 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());
        
    }

}

以下是上面程序的输出:

instanceOne hashCode=2011117821
instanceTwo hashCode=109647522

因此它也破坏了单例模式,这种情况下,我们需要实现readResolve()方法以阻止实例的创建:

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

然后就可以验证两个实例的hashCode相同。

你可能感兴趣的:(Singleton)