【通俗易懂】一天一个设计模式----单例模式

   日期:2020-07-13     浏览:84    评论:0    
核心提示:文章目录前言正文----单例模式----------有趣的小猪抢蛋糕----------单例模式前言

文章目录

前言
正文----什么是单例模式?
----------有趣的小猪抢蛋糕
----------单例模式

前言

设计模式代表了前人的最佳实践,在水平达到了一定的层级之后,总是要接触设计模式的。本文旨在使用通俗易懂的例子,帮助初学者理解单例模式。

什么是单例模式?

单例模式顾名思义,一个类仅能有一个实例。所以他的对象不能被手动实例化(就无法保证只有一个实例),而是由该类创建,并提供访问实例对象的方法。

有趣的小猪抢蛋糕

从前有一群笨笨的小猪,总共100头吧。

小猪的饮食很规律,吃饭前,如果桌子上没有蛋糕,第一头小猪申请100个小蛋糕放到桌子上,然后自己拿一个。如果桌子上有蛋糕,其他小猪只需要拿蛋糕就可以了,不需要再次申请。


  • Day1饲养员上班第一天,做蛋糕比较懒(懒汉模式),小猪申请完之后,需要等一会,才会把蛋糕放到桌子上。

    好多小猪同时到达饲养员身边,同时申请100个蛋糕,最后饲养员拿出了上千个蛋糕,猪圈公司损失惨重,饲养员被扣工资。

    Day2
    第二天,饲养员多了一个心眼,小猪先排队(加锁)。等第一个小猪申请完,并且等他把蛋糕放到桌子上,然后小猪依次排队一个个拿。这次蛋糕没有损失,但是效率很低。


    Day3
    第三天,饲养员发现效率还可以提高,一旦蛋糕放到了桌子上,小猪们就没必要排队,直接一人拿一个就可以了。但是在桌子上没有蛋糕时,还是要控制一下,第一个小猪申请完,此时桌子上并没有蛋糕(饲养员太懒了),其他小猪还会继续申请,这时候饲养员会告诉剩下的小猪蛋糕正在做,不用继续申请了。等到桌子上有了蛋糕,就不用告诉他们了。效率很高。



    Day4
    第四天,饲养员发现自己还是太懒了,变得勤快了(饿汉模式),他提前把蛋糕做好,然后提前放在桌子上,这样也就没有了小猪同时申请几千个蛋糕以及排队等待的情况了。


单例模式

我们从小猪抢蛋糕的故事来理解单例模式,饲养员做蛋糕的方式(懒惰或者勤快)对应我们两种方法(懒汉模式和饿汉模式)。

懒汉模式:(需要创建类我才创建,不需要创建我就不创建)

public class Cake{  
    private static Cake cake;  //蛋糕实例
    private Cake(){}; //构造方法
  
    public static Cake getHundredCake() {  //获取100个蛋糕
    if (cake == null) {  //如果桌子上没有蛋糕
        cake = new Cake();  
    }  
    return cake;  
    }  
}

饿汉模式: (不管你需不需要,我都先创建一个实例对象)

public class Cake{  
    private static Cake cake = new Cake();  //提前创建蛋糕实例
    private Cake(){}; //构造方法
  
    public static Cake getHundredCake() {  //获取100个蛋糕
    	return cake;  
    }  
}

在这里,线程就是我们的小猪,同时争抢的情况就是多线程冲突,不难发现饿汉模式对应饲养员第四天的情况,在饿汉模式下,对象实例已经被提前创建,牺牲了内存空间,但是解决了线程安全的问题,是线程安全的实现。

现在我们看一下懒汉模式,不难发现懒汉模式对应饲养员第一天的情况,在多线程情况下,多个线程同时申请创建实例,这样就创建了多个对象实例,不符合我们的单例设计模式。
比如下面的代码

if (cake == null) {  //如果桌子上没有蛋糕
        cake = new Cake();  
    }

第一个线程产生了一个cake引用对象,但此时还没有创建实例对象(没有调用new Cake()方法),然后后面的线程都突破了判断语句的屏障,都来实例化对象,对应于小猪同时申请蛋糕。

有人可能说为什么不先调用new Cake()方法,这里设计到一个指令重排的问题,感兴趣的朋友查看相关资料。

那怎么解决线程不安全的问题呢?参考饲养员第二天的做法,可以使用synchronized关键字。

我们在拿蛋糕的时候进行排队处理,(对获取实例方法进行加锁)

public class Cake{  
    private static Cake cake;  //蛋糕实例
    private Cake(){}; //构造方法
  
    public static synchronized Cake getHundredCake() {  //获取100个蛋糕,加上synchronized关键字,保证同步
    if (cake == null) {  //如果桌子上没有蛋糕
        cake = new Cake();  
    }  
    return cake;  
    }  
}

我们也说了,这种方式虽然能保证线程安全,但是效率不是很高。

对于提高效率的方法,我们参考饲养员第三天的做法,只对申请蛋糕(创建实例)进行排队(加锁),对于拿蛋糕不做限制,(细化锁的粒度,只对创建实例的步骤加锁)

public class Cake{  
    private static Cake cake;  //蛋糕实例
    private Cake(){}; //构造方法
  
    public static Cake getHundredCake() {  //获取100个蛋糕
    if (cake == null) {  //如果桌子上没有蛋糕
     synchronized (Singleton.class) {  //排队申请蛋糕
        cake = new Cake();  
        }
    }  
    return cake;  
    }  
}

但是现在这样真的可以线程安全了吗?

我们可以看到在对象被实例化之前,可能有多个线程突破了判断语句的屏障,我们现在加的同步锁只能保证这几个线程是依次创建实例对象,并不能保证单例。

(我们继续参考饲养员第四天的做法-------第一个小猪申请完,此时桌子上并没有蛋糕(饲养员太懒了),其他小猪还会继续申请,这时候饲养员会告诉剩下的小猪蛋糕正在做,不用继续申请了。)

我们需要告诉其他小猪,蛋糕正在做,怎么告诉呢?

这里我们可以在锁内部(蛋糕放到桌子之前),再加一个判断(告诉其他小猪不必申请),代码如下:

public class Cake{  
    private volatile static Cake cake;  //蛋糕实例
    private Cake(){}; //构造方法
  
    public static Cake getHundredCake() {  //获取100个蛋糕
    if (cake == null) {  //如果桌子上没有蛋糕
     synchronized (Singleton.class) {  //排队申请蛋糕
       if (cake == null) {  //如果正在做,但是还没放到桌子上
           cake = new Cake();  
        }
    }  
    return cake;  
    }  
}

我们可以看到现在有了两个判断语句和一个同步锁,我们一般称为
双重检查锁double-checked locking

值得注意的是我们的coke对象前加了volatile关键字修饰,该关键字能够防止指令重排以及保证内内存可见性,加上他后可以更加保证线程安全,感兴趣的朋友查看相关资料。

希望大家看完此文能够对单例模式有一个比较清晰的认识,码字不易,尊重原创,转载请加入本文链接—查看原文。

 
打赏
 本文转载自:网络 
所有权利归属于原作者,如文章来源标示错误或侵犯了您的权利请联系微信13520258486
更多>最近资讯中心
更多>最新资讯中心
0相关评论

推荐图文
推荐资讯中心
点击排行
最新信息
新手指南
采购商服务
供应商服务
交易安全
关注我们
手机网站:
新浪微博:
微信关注:

13520258486

周一至周五 9:00-18:00
(其他时间联系在线客服)

24小时在线客服