文章目录
前言
正文----什么是单例模式?
----------有趣的小猪抢蛋糕
----------单例模式
前言
设计模式代表了前人的最佳实践,在水平达到了一定的层级之后,总是要接触设计模式的。本文旨在使用通俗易懂的例子,帮助初学者理解单例模式。什么是单例模式?
单例模式顾名思义,一个类仅能有一个实例。所以他的对象不能被手动实例化(就无法保证只有一个实例),而是由该类创建,并提供访问实例对象的方法。有趣的小猪抢蛋糕
从前有一群笨笨的小猪,总共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关键字修饰,该关键字能够防止指令重排以及保证内内存可见性,加上他后可以更加保证线程安全,感兴趣的朋友查看相关资料。
希望大家看完此文能够对单例模式有一个比较清晰的认识,码字不易,尊重原创,转载请加入本文链接—查看原文。