设计模式-单例(译)

164 阅读6分钟

概要

单例模式是一种创建型设计模式(Creational Design Pattern),也是GOF23种设计模式中的一种。 从定义来看,这是一种非常简单的设计模式,但是在实现时还是又很多的考量。 在Java中实现单例模式,在开发者中是一个很有争议的话题,我们在这儿学习单例模式的原则,以及不同的实现方式,还包括使用时一些最佳实践

介绍

  • 单例模式严格限制类的实例化,并且确保在JVM中只有一个实例
  • 单例类必须提供一个全局的访问入口,用来获取单例实例
  • 单例模式通常用被用于实现日志(Logging),驱动(Driver),缓存(Caching),线程池(Thread Pool)
  • 单例模式也被用在一些其它的设计模式中,比如抽象工厂(Abstract Factory),建造者模式(Builder),原型模式(Propotype),门面模式(Facade)等
  • java的一些核心类也使用了单例设计模式,比如java.lang.Runtimejava.awt.Desktop

Java单例模式实现

我们有很多方法来实现单例模式,但是都要遵循下列的原则

  • 私有的构造器,限制从外部其它类实例化
  • 私有静态单例类变量,即唯一实例对象
  • 公共静态方法用于返回唯一实例对象,这是外部获取单例对象的唯一获取点

在下面的章节中,我们将用不同的方法来实现单例模式,并且考虑不同的实现的关注点

  • 饱汉初始化
  • 静态代码块初始化
  • 懒汉初始化
  • 线程安全单例
  • Bill Pugh实现
  • 反射破坏单例
  • 枚举实现
  • 序列化和单例

1. 饱汉初始化

在饱汉初始化中,在类加载时初始化单例对象,这是最简单的一种单例实现方式。但是它有一个缺点,可能应用程序没有获取和使用单例对象,但都会创建它

下面是静态初始化单列类

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

2. 静态代码块初始化

静态代码块初始化和饱汉初始化类似,区别在于它是在静态代码块中初始化单例对象,这为处理异常提供了可能

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

不管是饱汉初始化还是静态代码块初始化来创建单例对象,都是在使用单例对象前就创建了实例,这并不是一个很好的实践。所以在下面的章节中,我们将学习如何通过延迟初始化来创建单例类

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

上面的实现范式在单线程环境下没啥问题,但是在多线程场景下会有问题。多线程同时访问时,将破坏单例模式,不同的线程可能获取到不同实例对象,在下面的章节中,我们可以用不同的方式来实现线程安全的单例类

4. 线程安全单例

把全局访问方法设置为线程安全的Synchronized是一种简单的解决方式,这样在同一个时刻只会有一个线程访问该方法。常见的实现方法代码如下

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 )。 在此方法中,同步代码块中有一次额外的检查来保证只有一个单例对象被创建。

下面是双重锁单例的代码块

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

5. Bill Pugh 实现

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

可以看到private inner static class 包含了一个单例实例,当单例类被加载,SingletonHelper 此时并没有被加载,并且只有在被调用 getInstance 方法时,该类才会被加载,这时单例实例会被初始化

这是一个被广泛使用的单例实现方法,它不依赖于同步。我也在不同的项目中使用这种方式,而且它也非常的容易被理解和实现

6. 使用反射破坏单例

Reflection can be used to destroy all the above singleton implementation approaches. Let’s see this with an example class.

反射可以用来破坏上面所有的单例实现模式,让我们看一个例子

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

7. 枚举单例

为了避免反射场景, Joshua Bloch建议使用枚举来实现单例,Java中保证枚举中的任意枚举值只被初始化一次。因为枚举的值可以被全局访问,所以是一个单例。但枚举这种方式也缺乏弹性,举个例子,他不能够被赖加载

package com.journaldev.singleton;

public enum EnumSingleton {

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

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();
        
        //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是一样的 我希望这篇文章能够帮助你很好的掌握单例模式的细节,如果你有一些想法可以留言让我们知道

原文链接