【设计模式】之单例模式

单例模式

什么是单例模式

单例模式属于管理实例的创造型类型模式。单例模式保证在你的应用种最多只有一个指定类的实例。

单例模式应用场景

  • 项目配置类

读取项目的配置信息的类可以做成单例的,因为只需要读取一次,且配置信息字段一般比较多节省资源。通过这个单例的类,可以对应用程序中的类进行全局访问。无需多次对配置文件进行多次读取。

  • 应用日志类

日志器Logger在你的应用中是无处不在的。也应该只初始化一次,但是可以到处使用。

  • 分析和报告类

如果你在使用一些数据分析工具例如Google Analytics。你就可以注意到它们被设计成单例的,仅仅初始化一次,然后在用户的每一个行为中都可以使用。

单例模式简图

实现单例模式的类

  • 将默认的构造器设置为private。阻止其他类从应用中直接初始化该类。

  • 创建一个public static 的静态方法。该方法用于返回一个单例类实例。

  • 还可以选择懒加载初始化更友好。

示例代码

示例代码参见以下类

  • org.byron4j.cookbook.designpattern.singleton.Singleton
public class Singleton {

    private static Singleton instance;

    // 构造器私有化
    private Singleton(){

    }

    // 提供静态方法
    public static Singleton getInstance(){

        // 懒加载初始化,在第一次使用时才创建实例
        if(instance == null){
            instance = new Singleton();
        }
        return  instance;
    }


    public void display(){
        System.out.println("Hurray! I am create as a Singleton!");
    }


}

单元测试类:

package org.byron4j.cookbook.designpattern;

import org.byron4j.cookbook.designpattern.singleton.Singleton;
import org.junit.Test;

import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SingletonTest {

    @Test
    public void test(){
        final Set<Singleton> sets = new HashSet<>();

        ExecutorService es = Executors.newFixedThreadPool(10000);

        for(int i = 1; i <= 100000; i++){
            es.execute(new Runnable() {
                public  void run(){
                    Singleton s = Singleton.getInstance();
                    sets.add(s);
                }
            });
        }

        System.out.println(sets);

    }
}

运行输出如下,结果生成了多个Singleton实例:

[org.byron4j.cookbook.designpattern.singleton.Singleton@46b91344, org.byron4j.cookbook.designpattern.singleton.Singleton@1f397b96]

线程安全的单例模式

线程安全对于单例类来说是非常重要的。上述Singleton类是非线程安全的,因为在线程并发的场景下,可能会创建多个Singleton实例。

为了规避这个问题,我们可以将 getInstance 方法用同步字 synchronized 修饰,这样迫使线程等待直到前面一个线程执行完毕,如此就避免了同时存在多个线程访问该方法的场景。

public static synchronized Singleton getInstance() {
        
        // Lazy initialization, creating object on first use
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
}

这样确实解决了线程安全的问题。但是,synchronized关键字存在严重的性能问题。我们还可以进一步优化 getInstance 方法,将实例同步,将方法范围缩小:

public static Singleton getInstance() {

        // Lazy initialization, creating object on first use
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }

    return instance;

}

单元测试三种方式耗时比较:

package org.byron4j.cookbook.designpattern;

import org.byron4j.cookbook.designpattern.singleton.Singleton;
import org.byron4j.cookbook.designpattern.singleton.SingletonSynchronized;
import org.byron4j.cookbook.designpattern.singleton.SingletonSynchronizedOptimized;
import org.junit.Test;

import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SingletonTest {

    @Test
    public void test(){
        final Set<Singleton> sets = new HashSet<>();
        long startTime = System.currentTimeMillis();
        ExecutorService es = Executors.newFixedThreadPool(10000);

        for(int i = 1; i <= 100000; i++){
            es.execute(new Runnable() {
                public  void run(){
                    Singleton s = Singleton.getInstance();
                    sets.add(s);
                }
            });
        }
        System.out.println("test用时:" + (System.currentTimeMillis() - startTime));
        System.out.println(sets);

    }

    @Test
    public void testSynchronized(){
        final Set<SingletonSynchronized> sets = new HashSet<>();
        long startTime = System.currentTimeMillis();
        ExecutorService es = Executors.newFixedThreadPool(10000);

        for(int i = 1; i <= 100000; i++){
            es.execute(new Runnable() {
                public  void run(){
                    SingletonSynchronized s = SingletonSynchronized.getInstance();
                    sets.add(s);
                }
            });
        }
        System.out.println("testSynchronized用时:" + (System.currentTimeMillis() - startTime));
        System.out.println(sets);

    }

    @Test
    public void testOptimised(){
        final Set<SingletonSynchronizedOptimized> sets = new HashSet<>();
        long startTime = System.currentTimeMillis();
        ExecutorService es = Executors.newFixedThreadPool(10000);

        for(int i = 1; i <= 100000; i++){
            es.execute(new Runnable() {
                public  void run(){
                    SingletonSynchronizedOptimized s = SingletonSynchronizedOptimized.getInstance();
                    sets.add(s);
                }
            });
        }

        System.out.println("testOptimised用时:" + (System.currentTimeMillis() - startTime));
        System.out.println(sets);

    }
}

运行测试用例,输出如下:

test用时:1564
[org.byron4j.cookbook.designpattern.singleton.Singleton@68eae58e]

testSynchronized用时:3658
[org.byron4j.cookbook.designpattern.singleton.SingletonSynchronized@36429a46]

testOptimised用时:2254
[org.byron4j.cookbook.designpattern.singleton.SingletonSynchronizedOptimized@21571826]


可以看到,最开始的实现方式性能是最好的,但是是非线程安全的;
Synchronized 锁住整个getInstance方法,可以做到线程安全,但是性能是最差的;
缩小Synchronized范围,可以提高性能。

防止对象克隆破坏单例模式Singleton

涉及单例类时还要注意clone方法的正确使用:

package org.byron4j.cookbook.designpattern.singleton;

/**
 * 单例模式实例
 * 1. 构造器私有化
 * 2. 提供静态方法供外部获取单例实例
 * 3. 延迟初始化实例
 */
public class SingletonZClone implements  Cloneable{

    private static SingletonZClone instance;

    // 构造器私有化
    private SingletonZClone(){

    }

    // 提供静态方法
    public static SingletonZClone getInstance(){

        // 将同步锁范围缩小,降低性能损耗
        if(instance == null){
            synchronized (SingletonZClone.class){
                if(instance == null){
                    instance = new SingletonZClone();
                }
            }
        }
        return  instance;
    }

    /**
     * 克隆方法--改为public
     * @return
     * @throws CloneNotSupportedException
     */
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    public void display(){
        System.out.println("Hurray! I am create as a SingletonZClone!");
    }


}

默认情况下clone时protected修饰的,这里改为了public修饰,测试用例如下:

@Test
    public void testClone() throws CloneNotSupportedException {
        SingletonZClone singletonZClone1 = SingletonZClone.getInstance();
        SingletonZClone singletonZClone2 = SingletonZClone.getInstance();
        SingletonZClone singletonZClone3 = (SingletonZClone)SingletonZClone.getInstance().clone();

        System.out.println(singletonZClone1 == singletonZClone2);
        System.out.println(singletonZClone1 == singletonZClone3);
        System.out.println(singletonZClone2 == singletonZClone3);

    }

输出如下:

true
false
false

我们了解一下clone方法的API解释, clone 后的对象虽然属性值可能是一样的,但是已经不是同一个对象实例了:

x.clone() != x

x.clone().getClass() == x.getClass()

x.clone().equals(x)

clone方法返回一个被克隆对象的实例的副本,除了内存地址其他属性值都是一样的,所以副本和被克隆对象不是同一个实例。
可以看出clone方法破坏了单例类,为防止该问题出现,我们需要禁用clone方法,直接改为:

 /**
     * 克隆方法--改为public
     * @return
     * @throws CloneNotSupportedException
     */
    @Override
    public Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }

防止序列化破坏单例模式

Java序列化机制允许将一个对象的状态转换为字节流,就可以很容易地存储和转移。
一旦对象被序列化,你就可以对其进行反序列化--将字节流转为对象。
如果一个Singleton类被序列化,则可能创建重复的对象。
我们可以使用钩子hook,来解释这个问题。

readResolve()方法

在Java规范中有关于readResolve()方法的介绍:

对于可序列化的和外部化的类,readResolve() 方法允许一个类可以替换/解析从流中读取到的对象。
通过实现 readResolve 方法,一个类就可以直接控制反序列化后的实例以及类型。
定义如下:

  ANY-ACCESS-MODIFIER Object readResolve()
              throws ObjectStreamException;

readResolve 方法会在ObjectInputStream 从流中读取一个对象时调用。ObjectInputStream 会检测类是否定义了 readResolve 方法。
如果 readResolve 方法定义了,会调用该方法用于指定从流中反序列化后作为返回的结果对象。
返回的类型要与原对象的类型一致,不然会出现 ClassCastException。

@Test
    public void testSeria() throws Exception {
        SingletonZCloneSerializable singletonZClone1 = SingletonZCloneSerializable.getInstance();


        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.ser"));
        oos.writeObject(singletonZClone1);
        oos.close();
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.ser"));
        SingletonZCloneSerializable test = (SingletonZCloneSerializable) ois.readObject();
        ois.close();
        System.out.println(singletonZClone1 == test);

    }

测试输出: false; 说明反序列化的时候已经不是原来的实例了,如此会破坏单例模式。

所以我们可以覆盖 readResolve 方法来解决序列化破坏单例的问题:

类 SingletonZCloneSerializableReadResolve 增加 readResolve 方法:

/**
     * 反序列化时返回instance实例,防止破坏单例模式
     * @return
     */
    protected Object readResolve(){
        return getInstance();
    }

执行测试用例:

@Test
    public void testSReadResolve() throws Exception {
        
         s = SingletonZCloneSerializableReadResolve.getInstance();


        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.ser"));
        oos.writeObject(s);
        oos.close();
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.ser"));
        SingletonZCloneSerializableReadResolve test = (SingletonZCloneSerializableReadResolve) ois.readObject();
        ois.close();
        System.out.println(s == test);

    }

输出true,有效防止了反序列化对单例的破坏。

使用单例模式的注意事项

  • 单例类是很少使用的,如果你要使用这个设计模式,你必须清楚的知道你在做什么。因为全局范围内仅仅创建一个实例,所以在资源受约束的平台是存在风险的。

  • 注意对象克隆。 单例模式需要仔细检查并阻止clone方法。

  • 多线程访问下,需要注意线程安全问题。

  • 小心多重类加载器,也许会破坏你的单例类。

  • 如果单例类是可序列化的,需要实现严格类型

一个友好的完整单例模式实例

package org.byron4j.cookbook.designpattern.singleton;

import java.io.Serializable;

/**
 * 单例模式实例
 * 1. 构造器私有化
 * 2. 提供静态方法供外部获取单例实例
 * 3. 延迟初始化实例
 */
public class SingletonZCloneSerializableReadResolve implements  Cloneable, Serializable {

    private static SingletonZCloneSerializableReadResolve instance;

    // 构造器私有化
    private SingletonZCloneSerializableReadResolve(){

    }

    // 提供静态方法
    public static SingletonZCloneSerializableReadResolve getInstance(){

        // 将同步锁范围缩小,降低性能损耗
        if(instance == null){
            synchronized (SingletonZCloneSerializableReadResolve.class){
                if(instance == null){
                    instance = new SingletonZCloneSerializableReadResolve();
                }
            }
        }
        return  instance;
    }

    /**
     * 克隆方法--改为public
     * @return
     * @throws CloneNotSupportedException
     */
    @Override
    public Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }



    public void display(){
        System.out.println("Hurray! I am create as a SingletonZCloneSerializableReadResolve!");
    }

    /**
     * 反序列化时返回instance实例,防止破坏单例模式
     * @return
     */
    protected Object readResolve(){
        return getInstance();
    }
}

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

推荐阅读更多精彩内容

  • 单例模式(SingletonPattern)一般被认为是最简单、最易理解的设计模式,也因为它的简洁易懂,是项目中最...
    成热了阅读 4,226评论 4 34
  • 前言 本文主要参考 那些年,我们一起写过的“单例模式”。 何为单例模式? 顾名思义,单例模式就是保证一个类仅有一个...
    tandeneck阅读 2,485评论 1 8
  • 1.单例模式概述 (1)引言 单例模式是应用最广的模式之一,也是23种设计模式中最基本的一个。本文旨在总结通过Ja...
    曹丰斌阅读 2,864评论 6 47
  • 事情发生在前几天,有两个比较要好的同事L和Q因为年终奖不是很开心。于是,Q约我和L一起吃饭吐槽。我们仨虽然不是同...
    小侠豆豆阅读 400评论 0 0
  • 走吧,快走向流浪的远方 那里承载了有关你的梦想 风雨无阻日夜兼程的赶往 赶向未来虚无缥缈的道路 那里没有给你遮蔽的...
    拾柒拾柒_阅读 117评论 0 1