Java中单例设计模式之最佳实践举例

单例模式(Singleton)是“四人组”(GoF)设计模式中的一种,归类于创建型模式。从定义上来看,它似乎是非常简单的设计模式,但是当去实现它时,却又带来了很多实现方面的担忧。单例模式的实现一直是开发者之间的一个富有争议的话题。在这里,我们将了解单例设计模式的原则,用不同的方式来实现单例模式以及一些使用上的最佳实践。

单例模式

单例模式限制了类的实例,并确保在Java虚拟机中有且仅有一个类的实例对象的存在。这个单例类必须提供一个全局的访问点来获得这个类的实例。单例模式一般用于日志类(logging),驱动程序对象,缓存以及线程池(thread pool)中。

单例设计模式也用于其它的设计模式中,如抽象工厂Abstract Factory)模式,创建者模式(Builder),原型模式(Prototype),门面模式(Facade)等等。单例设计模式也用于核心Java类中,例如java.lang.Runtime,java.awt.Desktop.

Java中的单例模式

当我们去实现单例模式时,有各种不同的方法,但所有的方法中都不外乎以下几点。

  • 私有构造函数,限制从其它类中来进行实例化。
  • 同一个类的私有静态变量,它的实例对象只有唯一一个。
  • 公有的静态方法,返回该类的实例对象,这是从外部获得这个类的实例对象的一个全局访问点。

在下面,我们将会学习单例模式实现的不同方法和它的设计与实现。
1.饿汉式初始化
2.静态块初始化
3.懒汉式初始化
4.单例模式的线程安全问题
5.Bill Pugh式单例模式实现
6.反射技术对单例模式的破坏
7.枚举单例模式
8.单例模式与序列化

饿汉式初始化

在饿汉式初始化中,单例模式的实例对象在类加载的时候就创建了,这是创建单例模式类的最简单的方法,但它有一个缺陷,就是当客户端可能不使用它的时候,实例对象还是会被创建。

下面是静态初始化单例类的实现。

EagerInitializedSingleton.java
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方法。此外,这种方式的实例化不能提供任何用于异常处理的选项。

静态块初始化

静态块(Static block)的初始化与饿汉式初始化相似,不同之处在于类的实例化提供了异常处理(exception handling)的选项。

StaticBlockSingleton.java
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;
    }
}

前面两种方式的初始化都是在使用之前就已经创建了实例对象,并不是使用单例设计模式的最佳方式。因此在下面的部分中,我们将学习怎样创建懒汉式的单例类。

知识扩展:Java static

懒汉式初始化

懒汉式初始化实现了在全局的访问方法中去创建单例模式类的实例对象。下面是用这种方式创建单例类的代码。

LazyInitializedSingleton.java
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;
    }
}

上面这种实现方法在单线程中工作的很好,但是在多线程的环境中,如果多个线程是在同一时间访问的话,它可能就会导致问题了。它将破坏单例模式,而且两个线程会得到不同的单例类的实例对象。在下面的部分中,我们将用不同的方式来创建线程安全的单例类。

单例模式的线程安全问题

创建一个线程安全的单例类的一个更简单的方法是使用全局同步访问方法,以便每次只有一个线程可以执行这个方法,一般这种方法的实现如下类所示。

ThreadSafeSingleton.java
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;
    }
     
}

上面这种方式工作的很好而且是线程安全的,但是它也由于同步方法的成本而降低了程序的性能,尽管我们只需要它在少数几个线程中可以创建单独的实例。为了避免每次都产生这种额外的开销,我们使用双重检测锁(double checked locking)原则。在这种方法中,同步块放在if条件中进行检查,以确保只有唯一一个单例类的实例对象被创建。

下面的代码片段提供了双重检测锁原则的实现。

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

知识扩展:Thread Safe Singleton Class

Bill Pugh式单例模式实现

在Java5之前,Java的内存模式有很多的问题,当多个线程同时去获得单例类的实例对象时,在某些情形下,以上那些方法的访问可能都会失败。因此Bill Pugh想出了一个不同的方法来创建单例类,即通过使用静态内部类。它的实现方式如下:

BillPughSingletong.java

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方法时,这个类才被加载,单例类对象才被创建。

这种单例模式类创建的方法使用的最广泛,因为它不需要额外的同步锁开销,而且也很容易理解和实现。

知识扩展:Java Nested Classes

反射技术对单例模式的破坏

反射可以用来摧毁上面所有的单例实现方法,让我们来看看下面这个例子吧。

ReflectionSingletonTest.java

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

当你运行这个测试类的时候,你会发现两个实例对象的哈希值不一样,可见单例模式已经被破坏了。反射是一种很强大,并且在框架中如SpringHibernate中使用的很多的技术。更多请查阅Java Reflection Tutorial.

枚举单例模式

为了克服上面反射的那种情况,Joshua Bloch建议使用枚举来实现单例设计模式,因为Java能确保任何枚举值在Java程序中仅被实例化一次。由于Java的枚举(Java Enum)是全局访问的,所以在单例模式中,使用起来可能不太灵活,比如,它不支持懒汉式初始化。

EnumSingleton.java

package com.journaldev.singleton;
 
public enum EnumSingleton {
 
    INSTANCE;
     
    public static void doSomething(){
        //do something
    }
}

知识扩展:Java Enum

单例模式与序列化

有时在分布式系统中,我们需要在单例类中实现序列化的接口,以便我们能将它的状态储存在文件系统中,而在以后的某个时间点,我们又能将其从文件系统中恢复进行使用。下面是一个很小的实现了序列化接口的单例类。

SerializedSingleton.java

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

这种序列化单例类的一个问题就是当我们反序列化它的时候,它会创建一个新的实例对象。看下面这段简单的程序。

SingletonSerializedTest.java

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();
}
这样之后,你会发现在测试代码中两个实例对象的哈希值是一样的啦。

知识扩展:Java Serialization and  Java Deserialization.

希望这篇文章能够帮助你抓住单例设计模式的要点。

想了解更多?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章