01 单例模式(Singleton Pattern)

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

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

Java Singleton

  • 单例模式限制类的实例化,确保只有一个实例在JAVA虚拟机中。
  • 单例模式必须提供一个全局访问来获取类的实例。
  • 单例模式用于日志记录(logging),驱动程序对象(drivers objects),缓存(caching)和线程池(thread pool)等。
  • Singleton设计模式也用于其他设计模式,如Abstract Factory,Builder,Prototype,Facade等。

Java Singleton Pattern

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

  • 构造函数私有化(private constructor),以限制被其它类初始化。
  • 类的内部提供一个私有的静态变量来保存该类的全局唯一实例。
  • 提供一个公共的静态方法返回该类的唯一实例,这是外部获取该单例类实例的全局唯一访问点。

在下面的章节中,我们将学习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的值一样了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,406评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,732评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,711评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,380评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,432评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,301评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,145评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,008评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,443评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,649评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,795评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,501评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,119评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,731评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,865评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,899评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,724评论 2 354

推荐阅读更多精彩内容