Java 单例实现解析

什么时候使用Singleton

Singleton指仅仅被实例化一次的类。Singleton通常用来代表那些本质上唯一的系统组件,比如文件系统,窗口管理器,日历等。Singleton的类会使客户端测试变得异常困难,因为无法给Singleton替换模拟实现,除非Singleton实现一个充当其类型的接口。


Java类的实例化

按照是否调用类的构造器,可以简单的将类实例化的方法分为两大类:通过构造器实例化和不通过构造器实例化。下面以实例化 Windows10FileSystem 类为例进行详细说明。

Windows10FileSystem.java

public class Windows10FileSystem {
    private String name;
    private String description;
    // use default constructor
    // getter/setter method
}

这个文件系统十分的“简陋”,只包括文件系统名称和文件系统描述。

通过构造器实例化类

  • 通过 new 关键字。

    Windows10FileSystem fileSystem = new Windows10FileSystem();
    
  • 通过 ClassnewInstance() 方法。

    Windows10FileSystem fileSystem = (Windows10FileSystem) Class.forName("Windows10FileSystem").newInstance();
    

    注意:forName的参数必须是类的全限定名,这里为了简单使用类名。

  • 通过 ConstructornewInstance() 方法。

    Constructor<Windows10FileSystem> constructor = Windows10FileSystem.class.getConstructor();
    Windows10FileSystem fileSystem = constructor.newInstance();
    

    上述方法只适用于构造器可被访问的场景,如果构造器为private,可以使用下面的方法访问构造器。

    如果 Windows10FileSystem的构造器是私有(private)的,借助 AccessibleObject.setAccessible(true) 可以调用私有的构造器:

    Constructor<Windows10FileSystem> constructor = Windows10FileSystem.class.getDeclaredConstructor();
    constructor.setAccessible(true);
    Windows10FileSystem fileSystem = constructor.newInstance();
    

不使用构造器实例化类

  • 通过 clone() 函数。

    首先Windows10FileSystem 类需要实现clone()方法:

    @Override
    public Windows10FileSystem clone() {
        Windows10FileSystem copyFileSystem = new Windows10FileSystem();
        copyFileSystem.setDescription(this.description);
        copyFileSystem.setName(this.name);
        return copyFileSystem;
    }
    
    Windows10FileSystem fileSystem = new Windows10FileSystem();
    fileSystem.setName("clone");
    fileSystem.setDescription("graphical operating system");
    
    Windows10FileSystem cloneFileSystem = fileSystem.clone();
    
  • 通过反序列化。

    假设保存对象的文件为:fileSystem.obj

    ObjectInputStream in = new ObjectInputStream(new FileInputStream("fileSystem.obj"));
    Windows10FileSystem fileSystem = (Windows10FileSystem) in.readObject();
    

Singleton的实现

实现Singleton的思路

上一节我们已经了解Java中实例化一个类的多种方法,而Singleton的目标就是要确保类仅仅只被实例化一次,为此我们需要控制类实例化的入口,或者控制入口方法的调用次数或者控制方法每次调用返回同一个对象,确保一个类只被实例化一次。

  • 构造器私有化,降低类构造器的可见范围。

    private Windows10FileSystem() {}
    
  • 构造器私有化虽然可以降低访问范围,但享有特权的客户端可以借助 AccessibleObject.setAccessible(true) 方法,通过反射机制调用私有的构造器,为了抵御这种攻击,需要修改私有构造器,在构造器第二次调用时抛出异常,阻止类被多次实例化。

    private static AtomicBoolean FIRST_INSTANTIATION = new AtomicBoolean(true);
    
    private Windows10FileSystem() {
        if (FIRST_INSTANTIATION.get()) {
            FIRST_INSTANTIATION.compareAndSet(true,false);
        } else {
            throw  new UnsupportedOperationException();
        }
    }
    

    注意:这里没有考虑并发调用构造器的问题,在Java中可以使用锁或synchronized简单的进行方法同步

  • 为了让Singleton支持序列化,只实现Serializable 接口是不够的,为了维护并保证Singleton,所有实例域都必须是瞬时(transient)的,并提供一个readResolve() 方法,该方法每次都返回同一个实例。

    private static Windows10FileSystem INSTANCE = new Windows10FileSystem();
    
    private Object readResolve() {
        return INSTANCE;
    }
    

    该方法防止攻击的原理和简单的模拟可以参考《Effective Java》中的第77条-对于实例控制,枚举类型优先于readResolve

  • 类不要实现 Cloneable接口和clone()方法。

    保证不可以调用对象的clone() 函数来实例化。

实现Singleton的三种方法

有多种方法可以实现Singleton,虽然每种方法的具体细节不一样,但是每种方法的目标都是相同的:确保类只被实例化一次。强烈推荐下文描述的第一种方法来实现Singleton:使用单元素枚举类型实现Singleton。

单元素枚举类型实现Singleton

Java从1.5发型版本开始支持通过枚举(enum)实现Singleton,下面以enum 实现一个Windows 10文件系统,这个文件系统非常的简陋,只提供了名字和描述两项基本信息,具体的代码如下:

public enum Windows10FileSystem {
    /**
     * singleton file system instance
     */
    INSTANCE("Windows 10", "graphical operating system");

    private String name;
    private String description;

    Windows10FileSystem(String name, String separator) {
        this.name = name;
        this.description = separator;
    }

    public String getBaseInfo() {
        return name + "\t" + description;
    }
}

使用enum 实现Singleton更加简洁,无偿提供了序列化机制,绝对防止多次实例化,即使是在面对复杂的序列化和反射攻击时,这种方法依然绝对可靠。强烈推荐使用该方法实现Singleton。

导出公有静态成员(final 域)实现Singleton
public class Windows10FileSystem implements Serializable {
    private static final AtomicBoolean FIRST_INSTANTIATION = new AtomicBoolean(true);
    public static final Windows10FileSystem INSTANCE = new Windows10FileSystem("Windows 10","graphical operating system");

    transient private String name;
    transient private String description;

    /**
     * 构造器私有化,并防止多次调用
     * @param name 文件系统名称
     * @param separator 文件系统描述
     */
    private Windows10FileSystem(String name, String separator) {
        if (FIRST_INSTANTIATION.get()) {
            FIRST_INSTANTIATION.compareAndSet(true,false);
            this.name = name;
            this.description = separator;
        } else {
            throw new UnsupportedOperationException("windows file system can only be instantiated once");
        }
    }

    /**
     * 防止反系列化攻击
     * @return file system object
     */
    private Object readResolve() {
        return INSTANCE;
    }

    public String getBaseInfo() {
        return name + "\t" + description;
    }
}

static变量的初始化顺序参考JLS 8.7

公有静态工厂方法实现Singleton

这种方法相比与导出公有静态成员(final 域)实现Singleton而言只是公有静态变量变成了一个工厂方法,每次调用工厂方法都返回同一个实例。

private static final Windows10FileSystem INSTANCE = new Windows10FileSystem("Windows 10","graphical operating system");
public static Windows10FileSystem getInstance() {
        return INSTANCE;
    }

两种实现Singleton方法的核心都是通过私有化构造器来控制类的实例化。公有域方法的主要优势在于,类的成员声明很清楚的表明这个类是一个Singleton(可读性强):公有的静态域是final的,所以该域将总是包含相同的对象。公有域在性能上已经不再拥有任何优势,现代化的JVM实现几乎都能将静态工厂方法的调用内联化。静态工厂方法的优势在于,它提供了更高的灵活性:在不改变API的条件下,我们可以改变该类是否是Singleton的想法。工厂方法返回该类的唯一实例,但是,这可以很容易的被修改,比如修改为每一个线程返回同一个实例。

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

推荐阅读更多精彩内容

  • JAVA相关基础知识 1、面向对象的特征有哪些方面 1.抽象: 抽象就是忽略一个主题中与当前目标无关的那些方面,以...
    yangkg阅读 661评论 0 1
  • 1、面向对象的特征有哪些方面 1.抽象:抽象就是忽略一个主题中与当前目标无关的那些方面,以便更充分地注意与当前目标...
    michaelgong阅读 815评论 0 1
  • 一. Java基础部分.................................................
    wy_sure阅读 3,805评论 0 11
  • 大行其道的知识体系概念,彭小六、秋叶老师、泳澄老师等等,大家都在倡导的建立一个自己的学习系统,学习-保存-共享-使...
    xuelinger8401阅读 239评论 0 1
  • 有时候,期待的不是一句“我爱”, 而是一句“我在”。 未经允许, 擅自特别喜欢你。 只是,终究只是我以为。 一辈子...
    未经允许的红玫瑰阅读 209评论 0 1