《设计模式》——单例模式

先扯两句

  经过了漫长的时间,六大设计原则终于结束了,也终于进入到了23中设计模式的学习了。不过设计原则只有六条都写了这么久,设计模式有足足23种,希望这次不要再懒惰了吧。
  厚着脸皮激励一下自己《设计模式》——目录,然后让我们进入正题。

正文

  虽然说单例模式,作为设计模式中最最简单,最最基础的部分,大家应该再熟悉不过了,不过依照惯例。还是先说说什么是“单例模式”吧。

单例模式定义

  先上官方的:

  Ensure a class has only one instance, and provide a global point of access to it.(确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。)

  其实如果说单例模式,我还是喜欢以前玩《奥拉星》的时候看到的一句话(这段可不是广告啊,虽然我真希望它是):
“我”是“我”唯一的朋友

  情节暂且不表,只是这句话打动了我,还好当时有截图,今天写博客的时候,也可以拿出来跟大家分享。说句题外话,其实在如今这个快餐式的大时代下,又有多少人不是兜兜转转最后才发现,原来真的“只有‘我’是‘我’最好的朋友,唯一的朋友呢。”
  而这里的“唯一”正是我们的单例模式,你快乐时想要跟他分享、你悲伤时想要跟他倾述、你困惑时想要跟他请教、你孤单时想要跟他在一起,这里预祝所有看到这篇文字的大家,都能找到你的那个“唯一”。
  那么今天就以交朋友为例,来讲述一下如何使用单例模式吧。

单例模式举例

无单例

  曾经的老头子我找朋友的时候,不知道怎么经营朋友之间的情感,只是听说请客吃饭能够交到朋友,等你有需要的时候,朋友就可以帮助自己了。

/**
 * 半寿翁
 */
class HalfLife {
    /**
     * 请吃饭
     *
     * @param otherOne 想要交朋友的人
     */
    public void havingDinner(OtherOne otherOne) {
        otherOne.makeFriend(this);
    }

    /**
     * 寻求帮助
     *
     * @param otherOne 找来帮助我的人
     */
    public void askForHelp(OtherOne otherOne) {
        otherOne.helpJudge(this);
    }
}

/**
 * 朋友
 */
class OtherOne {
    /**
     * 每个人与我交朋友的次数
     */
    private Map<Integer, Integer> makeFriendTimesMap = new HashMap<>();

    /**
     * 来与我交朋友的人
     *
     * @param halfLife 半寿翁
     */
    public void makeFriend(HalfLife halfLife) {
        int friendId = halfLife.hashCode();
        Integer makeFriendTimes = makeFriendTimesMap.get(friendId);
        if (null != makeFriendTimes) {
            ++makeFriendTimes;
        } else {
            makeFriendTimes = 1;
        }
        makeFriendTimesMap.put(friendId, makeFriendTimes);

        System.out.println(hashCode() + " 与 " + friendId + " 成了 " + makeFriendTimes + " 次朋友!");
    }

    /**
     * 判断是否帮忙
     *
     * @param halfLife 半寿翁
     */
    public void helpJudge(HalfLife halfLife) {
        int friendId = halfLife.hashCode();
        Integer makeFriendTimes = makeFriendTimesMap.get(friendId);
        if (null != makeFriendTimes) {
            System.out.println("我是" + hashCode() + " , " + friendId + " 与我交了 " + makeFriendTimes + " 次朋友,我要帮他!");
        } else {
            System.out.println("我是" + hashCode() + " , " + friendId + " 你谁啊!");
        }
    }
}

  于是我就出去找各种人吃饭,直到终于又一次我需要帮助。

@Test
public void makeFriends() {
    HalfLife halfLife = new HalfLife();
    halfLife.havingDinner(new OtherOne());
    halfLife.havingDinner(new OtherOne());
    halfLife.havingDinner(new OtherOne());
    halfLife.havingDinner(new OtherOne());
    
    halfLife.askForHelp(new OtherOne());
}

你谁啊

线程不安全单例

  显而易见,得到的结果是让人伤心的,可是为什么会这样呢?直到很久我才发现,原来请吃饭的时候,去大街上拉个人,人家就来了,并且认了我这个朋友。可当我需要帮助的时候,去大街上拉个人,人家是不会来帮忙的,更别说要跟我做朋友了。

  这个时候我才知道找一个人来维系感情是有多么重要。

/**
 * 朋友
 */
class OtherOne {

    /**
     * 单例对象
     */
    private static OtherOne instance;
    /**
     * 交朋友总次数
     */
    private static int count = 0;
    /**
     * 与我交朋友的次数
     */
    private final Map<Integer, Integer> makeFriendTimesMap;

    private OtherOne() {
        makeFriendTimesMap = new HashMap<>();
    }

    /**
     * 获取单例对象
     *
     * @return 单例对象
     */
    public static OtherOne getInstance() {
        if (null == instance) {
            instance = new OtherOne();
        }
        return instance;
    }

    ...
}

  确定了唯一的朋友以后,我在各种场合请请饭,请的都是他,这个时候再找他帮忙。

@Test
public void makeFriends() {
    HalfLife halfLife = new HalfLife();
    halfLife.havingDinner(OtherOne.getInstance());
    halfLife.havingDinner(OtherOne.getInstance());
    halfLife.havingDinner(OtherOne.getInstance());
    halfLife.havingDinner(OtherOne.getInstance());
    halfLife.askForHelp(OtherOne.getInstance());
}

我要帮他

线程安全单例

  可是大家都知道,成年人的世界,是不可能一顿饭就成为朋友的。同样,每个人也不是只等待你一个人请吃饭,所以当不同的人,在不同场合下都要请这个人的时候,我们这个可怜的单例朋友就崩溃了。

@Test
public void makeFriends() {
    final HalfLife halfLife = new HalfLife();
    for (int i = 0; i < 50; i++) {
        final HalfLife halfLifeOther = new HalfLife();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int j = 0; j < 50; j++) {
                    halfLife.havingDinner(OtherOne.getInstance());
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int j = 0; j < 50; j++) {
                    halfLifeOther.havingDinner(OtherOne.getInstance());
                }
            }
        }).start();
    }
}

  虽然我数学不太好,可能算不好500*500到底等于多少,但是最后个位数是8怎么都有些过分了吧。这样显然是记错了啊,虽然说从朋友的角度,我们请吃饭的没有那么在乎请了多少顿,可是想必大家身边应该会有那种人,别人请我一次,我一定要还回去。不说这种做法是否正确,但是这都记错了怎么还啊!

  这是为什么呢?自然就是我们的单例模式是非线程安全的,就好像是好朋友结婚,我们去参加婚礼,如果都是上午结婚,如果有一两个,那么我们肯定是都能去得上的,哪怕只是到现场敬个酒呢。

  可一旦这个数量变成,你从小学开始到初中、高中、大学、工作的同事都是同一天的上午结婚呢(这里就别想着人朋友们因为你一个人去重新调整婚礼的时间了,毕竟定个饭店也不容易,没听说预定一年后的婚礼,结果婚礼没办呢,两口子先离婚了)?哪怕是在同一个城市里,想要每个婚礼都去敬一杯酒是肯定不可能的了,除非他们能一起办一场云婚礼。

  而这个时候,我们自然就要做出取舍,从这些好朋友中选择最好的,或者最方便去的那些参加,其他的就只能微信支付宝转账了。

  所以我们非线程安全的单例模式就是这样舍弃了一些忙不过来的内容,甚至连转账都没给人家,这个时候自然就要失去一些朋友了,就算不至于这么严重,至少朋友心中多少会有一些不舒服的吧(只希望你的朋友不像我们的客户)。

  为了解决这个问题,就只能强化我们朋友的能力了,让他可以实现线程安全。

/**
 * 朋友
 */
class OtherOne {

    /**
     * 单例对象
     */
    private static OtherOne instance;
    /**
     * 与我交朋友的次数
     */
    private final Map<Integer, Integer> makeFriendTimesMap;

    private OtherOne() {
        makeFriendTimesMap = new HashMap<>();
    }

    /**
     * 获取单例对象
     *
     * @return 单例对象
     */
    public static OtherOne getInstance() {
        if (null == instance) {
            synchronized (OtherOne.class) {
                if (null == instance) {
                    instance = new OtherOne();
                }
            }
        }
        return instance;
    }

    ...
}

  下面我们就看一下为了线程安全,都做出了哪些调整吧:

  1. 在单例创建中使用synchronized(参见淘小笛【Java并发编程之深入理解】Synchronized的使用),实现各线程之间的同步
  2. 双重判空,首先判空是为了创建我们的单例,但是由于为了防止创建重复,我们加了synchronized,而当前一个线程执行完,锁释放后则会来执行synchronized中的代码,而由于我们无法确定前一个线程中是否已经完成了创建操作。因此这里又进行了一次判断,只有前一个线程没有创建的时候,我们才会创建新的单例。

PS:

  这里有件事需要说明一下,大家可以看到,我在后面线程安全的部分并没有截图。实在我跑了代码,但是残酷的现实是最后计数是错误的,而且错误的还很奇葩,如下是最简版的测试代码(天知道我开始为了显示实力创建了多少线程,那酸爽。。。):*

@Test
public void makeFriends() {
    final HalfLife halfLife = new HalfLife();
    for (int i = 0; i < 2; i++) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                final HalfLife halfLifeOther = new HalfLife();
                for (int j = 0; j < 5; j++) {
                    halfLife.havingDinner(OtherOne.getInstance());
                    halfLifeOther.havingDinner(OtherOne.getInstance());
                }
            }
        }).start();
    }
}

错误计数
  经过我仔细的计算:2x5x2=20,所以多出来的那两条数据,我实在是不知道是从哪来的,暂时只能猜测于是Android Studio分的内存少了,因为添加了synchronized,而上一次操作的锁没有完全释放,就执行了第二次才出现的。

  当然这里举例的是最奇葩的一种,大多数还是缺少数据的情况,但是除了《设计模式之禅》的方法以外,百度前几页的方法都尝试过了,效果都不是很理想,但是大家都这么写应该还是有一定道理的,我这里就先记录下来。毕竟一个移动端开发的,暂时也想不到特别好的多并发测试工具,只能后期接触到了再回来补充这个部分了。也欢迎各位大牛指点。

PS2:

  实现同步其实有很多参数,也有很多地方,刚开始实现的时候,我这里是将Map替换为了线程安全的ConcurrentMap,同时还在makeFriend的方法上添加了synchronized。就好像是一个孩子看到新玩具一样,满世界拿去玩,而出现的现象就是程序运行的时候时间相对较长,需要停顿很久重新执行方法,才能看到结果。当然这里没有拿监听程序去看具体的对比数据。不过现阶段的资料反馈来看,那些所谓的线程安全操作算是过度设计。因此如果是新手的话,建议还是先按照大牛的方式,只是将对应的工具类等进行线程安全设置,而类中的方法暂时就先不要过度处理了。如果真的出现了问题,再具体分析吧。

单例模式拓展:

  前面说了朋友,虽然有句话咋说来着“人生得一知己足以”,但是稍弱一级的朋友还是很难做到只有一个的,有一起玩游戏的、一起胡吃海塞的、一起环游世界的、或者哪怕只是无聊的时候一起胡吹的。而这种情况下,我们又应该如何处理呢?总不能盼着自己的朋友所思所想都与我们同步吧,所以我们肯定是要有多个朋友的;但同时,也不能像刚开始的时候一样,将全世界的人都当成自己的朋友,至少不会全世界的人都拿你当朋友。这个时候只能再次拓展我们的朋友类了。

  当然,看过我前面文章的应该知道,我一向是只借鉴《设计模式之禅》的目录结构,但是具体的例子都是自以为是的编,而这里环境已经换到了朋友上了,拓展的支持肯定也不能是书中的静态代码块的形式初始化了。

  想自作聪明,自然得为自己的自作聪明买单,那就是怎么实现多种朋友的效果,想了很久,忽然想到了与陌生人成为朋友前的第一件事,那就是自我介绍啊!想法有了,那么我们就开始动手调整了。

  1. 多个朋友,就需要缓存多个朋友的单例类,由于我们不能创建构造方法,因此这个缓存的读写自然是要在getInstance() 方法中,所以为了这里就只能使用线程安全的缓存工具
  2. 由于需要使用朋友的名字与单例类相对应,所以前面说得那么高端的线程安全缓存工具,其实就是个线程安全的Map罢了——ConcurrentMap,Key为姓名,Value为单例类
  3. 由于交朋友肯定不是出生的时候就带来的,而是需要一个一个的交,这也就是为什么这里不建议使用静态代码块直接生成的原因。(当然,把父母、兄弟姐妹、叔叔阿姨当成朋友的也是有的,所以这里也是可以将两种方案集成到一起的,我这里就先说说不适用静态代码块初始化生成的形式
/**
 * 朋友
 */
class OtherOne {
    /**
     * 单例对象缓存
     */
    private static final ConcurrentMap<String, OtherOne> instanceMap = new ConcurrentHashMap<>();

    /**
     * 获取单例对象
     *
     * @param name 朋友姓名
     * @return 单例对象
     */
    public static OtherOne getInstance(String name) {
        OtherOne instance = instanceMap.get(name);
        if (null == instance) {
            synchronized (OtherOne.class) {
                instance = instanceMap.get(name);
                if (null == instance) {
                    instance = new OtherOne();
                    instanceMap.put(name, instance);
                }
            }
        }
        return instance;
    }
    ...
}

  显而易见,其实改动也没多少,其实还是前面双重判空的流程只是判断的来源变成从ConcurrentMap中取出的参数了,如果有,那么则直接回传;如果没有这个单例对象,则创建一个新的。当然,回传之前别忘了添加到我们的缓存ConcurrentMap中就好。

单例模式的优缺点

  代码的部分结束了,又该上概念了。

优点

  1. 减少内存开支,就好像前面创建线程一样,无节制的创建导致日志打印都乱七八糟的了,这也就是为什么现在都要使用线程池控制。而创建类虽然没有线程那么耗资源,但蚊子再小也是肉啊,我们为什么不把这有限的资源投入到无限的创造价值,给老板的孩子赚奶粉钱中去呢?

  2. 减少性能开销,有句老话叫“多年的媳妇熬成婆”,被压榨久了,有机会了总想着报复回来,即便被报复的人早已经不是欺压你的人了。可是当婆最基本的要求就是有个儿子。而没多当一个人的婆,无论是再生一个儿子,还是强行拆散儿子儿媳,都是要对家庭这个系统造成严重的开销的,所以找一个欺负就可以了。至于不欺负,还没有女朋友的我,看我妈每天刷的抖音内容,认真程度就差记笔记的样子,那是100个不相信!!!

  3. 避免对资源的多重占用,这个最好理解了,想想上线前加班加点的你,突然被领导叫过去:“这个项目的使用说明文档出一下,明天一早要!”

  4. 设置全局访问点,好吧,这个称呼真的好高端,反应了半天,忽然想起来,不然就当工具类理解怎么样?

缺点

  1. 拓展困难,我们继承一个类的时候,是没法继承getInstance方法,如果是抽象方法,是没办法直接实现创建单例对象的业务逻辑的,只能在实际工作中,依靠大家的自觉性进行拓展了,至少对于新人来说是不友好的。

  2. 单例模式对测试是不利的。在并行开发环境中,如果单例模式没有完成,是不能进行测试的,没有接口也不能使用mock的方式虚拟一个对象。

  3. 单例模式与单一职责原则有冲突。一个类应该只实现一个逻辑,而不关心它是否是单例的,是不是要单例取决于环境,单例模式把“要单例”和业务逻辑融合在一个类中。

  作为一个小菜鸟,在使用过程中,真的没有发现后续的两点问题,但是去掉又怕耽误到一些能从中领悟到什么的大家,所以暂时就先复制过来吧,还是后续有深刻的理解以后再优化这个部分。

再扯两句

  这篇单例模式从想到要写,到真正写完,至少用了一周的时间,中间总有各种各样的借口告诉自己还有更重要的事情要做。今天总算开始写了,又由于线程安全的部分被狠狠抽了几个耳光后,差点又放弃,想要找到真正的解决方案。可思来想去,如果放下去找方案,估计又不知道要拖延到什么时候。所以最终还是选择将这块硬骨头生啃下来了,其中可能由于个人阅历问题,那里没有阐述清楚,请大家见谅,也欢迎大家在评论区指出,或拿出来一起讨论。

  其实“单例模式”应该是23中设计模式中最简单的一种了,原想是能很快就搞定的,却没想到今天竟然用了将近6个小时,之前写六大原则的应该都没有到这个时间的。也算是给了自己一个教训,不要小看任何看似简单的事,说不好它就会给你个惊喜,与大家共勉。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值