一、定义
单例模式:确保某个类仅有一个实例,且自行实例化并提供全局访问点。
二、应用场景
在某些情况下,一些对象我们只需要一个,比如一些工具类的对象,线程池,日志对象等。
三、类图
四、模式简单实现
首先,一个完美的单例模式应该保证哪些功能呢?
1.单例
2.延迟加载
3.线程安全
4.没有性能问题
5.防止序列化产生新对象
6.防止反射攻击
可以看出,要写好一个单例模式也不是件简单的事哦。
接下来我们讲解一下单例模式的实现方式及每种方式的优缺点。
大家平时去商场的时候应该都见过卖果汁的小店,我们就以这为例,讲解一下单例模式。
我们知道制作果汁需要榨汁机JuiceExtractor。我们只需要一个榨汁机即可,总不能每榨一杯果汁都要new一个榨汁机吧,那样肯定是不可行的,因此既然只需要一个榨汁机,那我们就把他设置成单例的。
public class JuiceExtractor {
private static JuiceExtractor mJuiceExtractor;
private JuiceExtractor(){}
public static JuiceExtractor getIntance(){
if(mJuiceExtractor == null){
mJuiceExtractor = new JuiceExtractor();
}
return mJuiceExtractor;
}
/**
* 榨汁
*/
public void extract(){
System.out.println("榨汁");
}
}
这就是单例模式中所谓的懒汉式,也就是说只有在需要的时候才会实例化。
但是这时候如果有多个人向榨汁机里加东西,就会造成混乱,也就是我们遇到的多线程会造成数据不同步。解决方法就是把getIntance()方法变为同步方法。
public static synchronized JuiceExtractor getIntance(){
if(mJuiceExtractor == null){
mJuiceExtractor = new JuiceExtractor();
}
return mJuiceExtractor;
}
这样我们就迫使每个人在使用榨汁机之前都要等别人使用完成才能使用。
我们知道同步会降低性能,每次进入该方法都会受到同步的影响,效率很低,我们来改善一下。
双重检验锁模式:
public static JuiceExtractor getIntance(){
if(mJuiceExtractor == null){
synchronized (JuiceExtractor.class){
if(mJuiceExtractor == null){
mJuiceExtractor = new JuiceExtractor();
}
}
}
return mJuiceExtractor;
}
该方式就是double-check,双检测模式,这种方式实现的懒加载,避免了每次进入该方法都会受到同步带来的影响,只有mJuiceExtractor == null即第一次调用的时候才会使用同步,这里有人要问了,为什么在同步快内还要再检查一次,当多个线程同时进入同步块外面的if判断时,如果同步快内不做判断就有可能出现多个实例,并且也避免不了序列化和反射造成的影响。
某些情况下双检测还是避免不了产生多个实例的问题。
接下来介绍其他几种单例模式的实现方式。
饿汉模式:
public class JuiceExtractor {
private static JuiceExtractor mJuiceExtractor = new JuiceExtractor();
private JuiceExtractor(){}
public static JuiceExtractor getIntance(){
return mJuiceExtractor;
}
}
饿汉模式顾名思义就是在没用使用之前就已经把实例创建出来了,并且静态变量保证了线程安全。这种方式没有实现懒加载,同时也避免不了反序列化和反射造成的影响。
并且饿汉模式在一些场景下无法使用,比如 JuiceExtractor 实例的创建是依赖参数或者配置文件的,在 getInstance() 之前必须调用某个方法设置参数给它,由于加载类后一开始就被初始化,那样这种单例写法就无法使用了。
当实现Serializable接口后,反序列化时单例会被破坏。解决办法就是重写readResolve,才能保证其反序列化依旧是单例。
public class JuiceExtractor implements Serializable{
private static JuiceExtractor mJuiceExtractor = new JuiceExtractor();
private JuiceExtractor(){}
public static JuiceExtractor getIntance(){
return mJuiceExtractor;
}
/**
* 如果实现了Serializable, 必须重写这个方法
*/
private Object readResolve() throws ObjectStreamException {
return mJuiceExtractor;
}
}
静态内部类模式:
public class JuiceExtractor {
private JuiceExtractor(){}
private static class SingletonHolder{
private static final JuiceExtractor mJuiceExtractor = new JuiceExtractor();
}
public static JuiceExtractor getIntance(){
return SingletonHolder.mJuiceExtractor;
}
}
这种方式与饿汉模式只要区别是,静态内部类模式不会在JuiceExtractor 初始化时立刻加载SingletonHolder,因此达到lazy loading效果,如果该单例比较耗资源可以使用这种模式。
枚举模式:
enum JuiceExtractorEnum{
INTANCE;
public void extract(){
System.out.println("榨汁");
}
}
该模式不仅能避免多线程同步问题,而且还能防止反序列化和反射重新创建新的对象,推荐使用该方式。
多个实例情况
public class JuiceExtractorSet{
private HashMap<String, JuiceExtractor> mExtractor;
private static JuiceExtractorSet mJuiceExtractorSet;
private JuiceExtractorSet(){
mExtractor = new HashMap<>();
}
public JuiceExtractorSet getIntance(){
if(mJuiceExtractorSet == null){
synchronized (JuiceExtractorSet.class){
if(mJuiceExtractorSet == null){
mJuiceExtractorSet = new JuiceExtractorSet();
}
}
}
return mJuiceExtractorSet;
}
public JuiceExtractor getJuiceExtractor(String name){
return mExtractor.get(name);
}
public void putJuiceExtractor(String name, JuiceExtractor juiceExtractor){
mExtractor.put(name, juiceExtractor);
}
}
我们可以利用单例模式和map来实现一些缓存的机制,而且我们还可以控制实例的个数,线程池就是利用上面这种方式。
五、总结
单例模式的原理就是将构造方法私有化,并提供给外界一个获取实例的静态方法,并且在获取过程中我们需要保证线程安全。
优点:
1.防止频繁的创建对象和销毁对象,提高性能。
2.全局只有一个实例,节省内存。
3.允许可变数量的实例,便于控制。
缺点:
1.兼容性比较差,不利于扩展。
2.使用不当容易造成内存泄漏。
注意:如果我们实现了Serializable接口,如果我们序列化了一个单例对象,当我们复原多次时,就会出现多个单例对象。