Java设计模式——单例模式(Singleton pattern)


众所周知,在代码中采用合理的设计模式,不仅仅能使代码更容易被他人理解,同时也能使整体模块拥有更合理的结构,方便后期扩展维护。因此就产生了一些“套路”,而这些“套路”我们便称之为“设计模式”。

另外,如果想要弄明白一些知识,一定要分清楚顺序,即遇到了什么问题要怎么解决以及有没有更好的办法,这样带着问题去思考,可以达到事半功倍的效果。

言归正传,开始说单例模式。按照上面的思考顺序,我们一步一步来分析。

1. 有本参奏,无本退朝

开始上早朝了啊~平常我们在使用某个类的实例时,直接使用关键字new,便可创建一个实例对象。但有时候可能会频繁使用某个实例对象,或者创建这个对象比较耗费资源,例如请了一个管家,需要管家帮你干一些事,总不能每次需要管家的时候就重新聘请一个吧?最好的方法就是长期聘请这个管家,需要的时候直接吩咐就行了。突然发现我这个例子举得是很恰当啊!

通过上面的阐述,我们遇到一个问题,那就是某个类的实例对象频繁使用,或者创建时比较费时费事时,希望只创建一次对象,并且一个就够了(你要是非得请两个管家,我只能说你有钱)。在这种情况下,我们来开始思考如果解决这个问题。

2. 建言献策,百花齐放

大家都开始献上良策啊,一个一个来,第一位,赵学士你先发言~

2.1 饿汉式

其实挺好解决的,看我下面的代码:

// 赵学士的方案
public class Singleton {
    private static Singleton sInstance = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
        return sInstance;
    }
}

构造方法私有化这就不解释了,保证外部不能随便通过new关键字来创建对象;静态成员变量sIntanceSingleton这个类加载的时候就初始化,创建了Singleton对象,并且只存在一个;通过Singleton.getInstance()方法可以获取该实例对象,这就是单例模式!这就解决了问题啊同志们!

不过钱大臣想了想说,不过这种方法好像有点弊端,假如我现在还不需要管家,总不能让我白花钱养着吧?能不能在我需要的时候再花钱聘请管家?

诶~~你这么一说也有道理啊,那钱大臣,说说你的办法。

2.2 懒汉式

话不多说,先看代码:

// 钱大臣的方案
public class Singleton {
    private static Singleton sInstance;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (sInstance == null) {
            sInstance = new Singleton();
        }
        return sInstance;
    }
}

怎么样?这个办法不错吧!成员变量默认初始化不创建对象,当调用Singleton.getInstance()方法时,如果sInstancenull再创建对象,否则就直接返回,保证了你的要求。

此时孙丞相“哼”了一下说,你这还不如赵学士呢!赵学士有可能提前白花钱聘请了一个管家,而你有可能多花钱请了好几个管家呢!你都没有考虑到多线程的情况!钱大臣一听赶紧做了修改,代码如下:

// 钱大臣的方案2
public class Singleton {
    private static Singleton sInstance;

    private Singleton() {
    }

    public synchronized static Singleton getInstance() {
        if (sInstance == null) {
            sInstance = new Singleton();
        }
        return sInstance;
    }
}

给getInstance方法加了关键字synchronized,保证创建对象的时候只有一个调用者,可以了吧?孙丞相又“哼”了一声,可每次调用的时候都会因为这个锁带来的时间开销,你以为开锁不要时间啊?性能低下!钱大臣脸有点红,于是又做了修改:

// 钱大臣的方案3
public class Singleton {
    private static Singleton sInstance;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (sInstance == null) {
            synchronized (Singleton.class) {
                sInstance = new Singleton();
            }
        }
        return sInstance;
    }
}

sInstance = new Singleton();语句加了锁,应该没问题了吧?孙丞相第三次“哼”了一声,我给你假设个情况啊,设现有线程A和B,在某个时刻两个线程都通过了判空语句但都没有取到锁资源,然后线程A先取得锁资源进入临界区(被锁的代码块),创建了一个对象,然后退出临界区,释放锁资源。接着线程B取得锁资源进入临界区,开始创建对象,退出临界区,释放锁资源,请问现在有几个Sinleton对象?钱大臣听后说那我直接把锁加到判空语句之前!

// 钱大臣的方案4
public class Singleton {
    private static Singleton sInstance;

    private Singleton() {
    }

    public static Singleton getInstance() {
        synchronized (Singleton.class) {
            if (sInstance == null) {
                sInstance = new Singleton();
            }
        }
        return sInstance;
    }
}

孙丞相直接笑了,说你这样和在方法上加synchronized关键字有什么区别。连续被怼,钱大臣感觉很没面子直接反驳道,you can you up, no can no bb!

2.3 双重校验锁DCL(double checked locking)

孙丞相大手一挥说道,看好了啊,今儿让我教教你怎么做人!

// 孙丞相的方案
public class Singleton {
    private static volatile Singleton sInstance;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (sInstance == null) {
            synchronized (Singleton.class) {
                if (sInstance == null) {
                    sInstance = new Singleton();
                }
            }
        }
        return sInstance;
    }
}

首先,方法锁改成代码块锁,减少锁的范围;其次第一次判空,在单线程的情况下提升了效率,但此时如果同时存在两个线程并发情况,即都判空成功,接下来会由锁内的第二次判空来过滤。还是刚才的例子,假设现有线程A和B,在某个时刻两个线程都通过了第一次判空语句但都没有取到锁资源。然后线程A先取得锁资源进入临界区(被锁的代码块),执行第二次判空语句,判空成功,创建了一个对象,然后退出临界区,释放锁资源。接着线程B取得锁资源进入临界区,执行判空语句发现不通过,直接退出临界区,释放锁资源。

另外在成员变量sInstance前面加了一个volatile关键字,这个特别重要。容我装个逼:在Java内存模型(JMM)中,并不限制处理器的指令顺序,说白了就是在不影响结果的情况下,顺序可能会被打乱。

在执行sInstance = new Singleton();这条命令语句时,JMM并不是一下就执行完毕的,即不是原子性,实质上这句命令分为三大部分:

  1. 为对象分配内存
  2. 执行构造方法语句,初始化实例对象
  3. 把sInstance的引用指向分配的内存空间

在JMM中这三个步骤中的2和3不一定是顺序执行的,如果线程A执行的顺序为1、3、2,在第2步执行完毕的时候,恰好线程B执行第一次判空语句,则会直接返回sInstance,那么此时获取到的sInstance仅仅只是不为null,实质上没有初始化,这样的对象肯定是有问题的!

volatile关键字的存在意义就是保证了执行命令不会被重排序,也就避免了这种异常情况的发生,所以这种获取单例的方法才是真正的安全可靠!

一直默默不做声的李将军冷不丁地开口了,孙丞相啊,你不觉得你这样写很麻烦吗?我有更简单的写法呢!

2.4 静态内部类实现的单例模式

你看你这又是判空又是加锁的,多麻烦,其实可以通过静态内部类的方式,既保证了只存在一个单例,又保证了线程安全,代码如下:

// 李将军的方案
public class Singleton {

    private Singleton() {
    }

    public static Singleton getInstance() {
        return SingletonHolder.sInstance;
    }

    private static class SingletonHolder {
        private static Singleton sInstance = new Singleton();
    }
}

当外部类Singleton被加载时,其静态内部类SingeletonHolder不会被加载,所以它的成员变量sInstance是不会被初始化的,只有当调用Singleton.getInstance()方法时,才会加载SingeletonHolder并且初始化其成员变量,而类加载时是线程安全的,这样既保证了延迟加载,也保证了线程安全,同时也简化了代码量,一举三得!

2.5 枚举单例

在说完上面4种单例模式的实现方式之后,不知道大家有没有想到过一个问题,那就是序列化。我们可以通过以下代码将实例写入磁盘,然后再从磁盘读出,即使构造方法是私有的,反序列化也是可以通过特殊的途径去重新创建一个新的实例,代码如下:

public Singleton createNewInstance() throws IOException, ClassNotFoundException {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(baos);
    // 此处的singleton为通过单例模式获取到的实例对象
    oos.writeObject(singleton);

    ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
    ObjectInputStream ois = new ObjectInputStream(bais);
    // 此时返回一个反序列化后得到的新的实例对象
    return (Singleton) ois.readObject();
}

可以通过上面的代码看到,反序列化后可以得到一个新的实例对象,那么这种现象没法避免了吗?其实是可以避免的。反序列化提供了一个很特别的方法,即一个私有、被实例化的方法readResolve(),这个方法可以让开发人员控制对象的反序列化。想要杜绝上面现象的发生,那么就可以在单例模式中加入readResolve()方法,代码如下:

private Object readResolve() {
    // 此处返回单例模式中的实例对象
    return sInstance;
}

在《Effective Java》一书中,作者Joshua Bloch提倡可以采用枚举的方式来解决上述出现的所有问题,代码如下:

// 外国老大哥Joshua Bloch的方案
public enum SingletonEnum {
    INSTANCE;
    
    public void method(){
        // do something...
    }
}

可以通过SingletonEnum.INSTANCE获取单例,然后再调用内部的各种方法。枚举实现单例有如下好处:

  1. 实例的创建线程安全,确保单例;
  2. 防止被反射创建多个实例;
  3. 没有序列化的问题。

虽然这种方法还没有被广泛采用,但是单元素的枚举类型已经成为实现Singleton单例模式的最佳方法。

3. 总结

通过上面的一步步分析,不知道大家有没有对单例模式有个新的认识呢?总的来说,加了volatile关键字的双重校验锁和静态内部类实现的单例模式是目前应用最为广泛的,如果你们要求更严的话,那么枚举单例也不失为一个获取单例更加的方式。欢迎各位能多多交流,指出不足,共同学习进步!

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

推荐阅读更多精彩内容