我爱编程

01 单例模式(Singleton Pattern)

2018-05-24  本文已影响0人  智行孙

一句话概括:限制类的实例化,全局最多只有一个该类的实例。

单例模式是四大模式之一,概念很简单,但实现起来会有诸多问题。

Java Singleton

Java Singleton Pattern

实现Singleton模式有多种方法,但是它们都有以下共同的概念。

在下面的章节中,我们将学习Singleton模式实现的不同方法以及各个实现中涉及的问题。

Eager initialization

Eager initialization是指Singleton类的实例在类被加载时创建,这是创建单例类最简单的方法。

package com.journaldev.singleton;

public class EagerInitializedSingleton {

    //在类被加载时就初始化该类的实例
    private static final EagerInitializedSingleton instance = new EagerInitializedSingleton();

    //防止其它成员初始化该类
    private EagerInitializedSingleton(){}

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

若你的单例类没有使用太多的资源,这种方法比较合适。但在大多数情况下,Singleton类是为文件系统,数据库连接等资源创建的。
除了调用getInstance方法获取实例外,我们应该避免被其它类实例化。另外注意,这种方法在实例化时没有办法提供任何异常处理选项。

Lazy Initialization

在全局初次使用该实例的时候进行初始化
示例代码:

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

上面的两个实现在单线程环境下工作良好,但是当涉及到多线程时,如果多个线程进入getInstance()方法的if循环内部,则会导致问题(尤其是在类的实例化需要一定时间时经常发生)。 它会破坏单例模式,两个线程都会得到单例类的不同实例。 接下来我们来讨论创建线程安全的单例类的方法。

Thread Safe Singleton

创建线程安全单例类的更简单方法是使全局访问方法同步,以便一次只有一个线程可以执行此方法。

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

以上方法能在多线程下工作良好提供了线程安全的方式,但是牺牲了性能,因为增加了同步方法(synchronized),其实我们仅仅需要用在可能会导致单独创建实例的前几个线程(参考:Java多线程相关)。为了避免每次额外的开销,我们可以使用双重检查锁定(double-checked locking),在if条件中使用synchronized块进行附加检查,以确保只创建一个singleton类实例。

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

Bill Pugh Singleton Implementation

在Java 5之前,Java内存模型存在很多问题,以上方法在某些情况下会失败,因为太多线程试图同时获取Singleton类的实例。 所以Bill Pugh提出了一种使用内部静态帮助类来创建Singleton类的另一种方法。 Bill Pugh Singleton的实现是这样的:

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 static class SingletonHelper私有静态内部类,当单例类被加载时,SingletonHelper类没有被加载到内存,仅当有人调用了getInstance方法时,这个私有类才会有加载并创建单例类的实例。

这是Singleton类最广泛使用的方法,因为它不需要同步。我在很多项目中都使用这种方法,并且很容易理解和实施。

Using Reflection to destroy Singleton Pattern

用反射来破坏单例模式,反射可以破坏上述所有单例模式实现,我们来看个例子:

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

}

当你运行上述测试代码,你会发现两个instance的hashCode是不同的,破坏了单例模式。反射非常强大,在大量的类库里被用到,例如Spring和Hibernate.

Enum Singleton

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

package com.journaldev.singleton;

public enum EnumSingleton {

    INSTANCE;

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

Serialization and Singleton

有时在分布式系统中,我们需要在Singleton类中实现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的值一样了。

上一篇下一篇

猜你喜欢

热点阅读