设计模式之单例设计模式

单例设计模式

  • [2.1 孤独的太阳盘古开天,造日月星辰。](#2.1 孤独的太阳盘古开天,造日月星辰。)
  • [2.2 饿汉造日](#2.2 饿汉造日)
  • [2.3 懒汉的队伍](#2.3 懒汉的队伍)
  • [2.4 大道至简](#2.4 大道至简)

读《秒懂设计模式总结》

单例模式(Singleton)是一种非常简单且容易理解的设计模式。顾名思义,单例即单一的实例,确切地讲就是指在某个系统中只存在一个实例,同时提供集中、统一的访问接口,以使系统行为保持协调一致。singleton一词在逻辑学中指"有且仅有一个元素的集合",这非常恰当地概括了单例的概念,也就是"一个类仅有一个实例"。

2.1 孤独的太阳盘古开天,造日月星辰。

从"夸父逐日"到"后羿射日",太阳对于我们的先祖一直具有着神秘的色彩与非凡的意义。随着科学的不断发展,我们逐渐揭开了太阳系的神秘面纱。我们可以把太阳系看作一个庞大的系统,其中有各种各样的对象存在,丰富多彩的实例造就了系统的美好。这个系统里的某些实例是唯一的,如我们赖以生存的恒星太阳。

与其他行星或卫星不同的是,太阳是太阳系内唯一的恒星实例,它持续提供给地球充足的阳光与能量,离开它地球就不会有今天的勃勃生机,但倘若天上有9个太阳,那么将会带来一场灾难。太阳东升西落,循环往复,不多不少仅此一例。

2.2 饿汉造日

既然太阳系里只有一个太阳,我们就需要严格把控太阳实例化的过程。我们从最简单的开始,先来写一个Sun类。请参看代码 。

java 复制代码
public class Sun {
    
}

太阳类Sun中目前什么都没有。接下来我们得确保任何人都不能创建太阳的实例,否则一旦程序员调用代码"new Sun()",天空就会出现多个太阳,便又需要"后羿"去解决了。有些读者可能会疑惑,我们并没有写构造器,为什么太阳还可以被实例化呢?这是因为Java可以自动为其加上一个无参构造器。为防止太阳实例泛滥将世界再次带入灾难,我们必须禁止外部调用构造器,请参看代码

java 复制代码
public class Sun {
    private void Sun(){} // 构造器私有化
}

我们在第3行将太阳类Sun的构造方法设为private,使其私有化,如此一来太阳类就被完全封闭了起来,实例化工作完全归属于内部事务,任何外部类都无权干预。既然如此,那么我们就让它自己创建自己,并使其自有永有

java 复制代码
public class Sun {
    private static final Sun sun = new Sun();

    public Sun() {} // private constructor  
}

代码第3行中"private"关键字确保太阳实例的私有性、不可见性和不可访问性;而"static"关键字确保太阳的静态性,将太阳放入内存里的静态区,在类加载的时候就初始化了,它与类同在,也就是说它是与类同时期且早于内存堆中的对象实例化的,该实例在内存中永生,内存垃圾收集器(Garbage Collector, GC)也不会对其进行回收;"final"关键字则确保这个太阳是常量、恒量,它是一颗终极的恒星,引用一旦被赋值就不能再修改;最后,"new"关键字初始化太阳类的静态实例,并赋予静态常量sun。这就是"饿汉模式"(eager initialization),即在初始阶段就主动进行实例化,并时刻保持一种渴求的状态,无论此单例是否有人使用。单例的太阳对象写好了,可一切皆是私有的,外部怎样才能访问它呢?正如同程序入口的静态方法main(),它不需要任何对象引用就能被访问,我们同样需要一个静态方法getInstance()来获取太阳的单例对象,同时将其设置为"public"以暴露给外部使用

java 复制代码
public class Sun {
    private static final Sun sun = new Sun();

    public Sun() {} // private constructor

    public static Sun getInstance(){
        return sun;
    }
}

太阳单例类的雏形已经完成了,对外部来说只要调用Sun.getInstance()就可以得到太阳对象了,并且不管谁得到,或是得到几次,得到的都是同一个太阳实例,这样就确保了整个太阳系中恒星太阳的唯一合法性,他人无法伪造。当然,读者还可以添加其他功能方法,如发光和发热等,此处就不再赘述了。

2.3 懒汉的队伍

至此,我们已经学会了单例模式的"饿汉模式",让太阳一开始就准备就绪,随时供应免费日光。然而,如果始终没人获取日光,那岂不是白造了太阳,一块内存区域被白白地浪费了?这正类似于商家货品滞销的情况,货架上堆放着商品却没人买,白白浪费空间。因此,商家为了降低风险,规定有些商品必须提前预订,这就是"懒汉模式"(lazy initialization)。沿着这个思路,我们继续对太阳类进行改造

java 复制代码
public class Sun {
    private static Sun sun = new Sun();

    public Sun() {} // private constructor

    public static Sun getInstance(){

        if (null == sun) {
            sun = new Sun(); //沒有sun才构造
        }
        return sun;
    }
}

可以看到我们一开始并没有造太阳,所以去掉了关键字final,只有在某线程第一次调用第9行的getInstance()方法时才会运行对太阳进行实例化的逻辑代码,之后再请求就直接返回此实例了。这样的好处是如无请求就不实例化,节省了内存空间;而坏处是第一次请求的时候速度较之前的饿汉初始化模式慢,因为要消耗CPU资源去临时造这个太阳(即使速度快到可以忽略不计)。这样的程序逻辑看似没问题,但其实在多线程模式下是有缺陷的。试想如果是并发请求的话,程序第10行的判空逻辑就会同时成立,这样就会多次实例化太阳,并且对sun进行多次赋值(覆盖)操作,这违背了单例的理念。我们再来改良一下,把请求方法加上synchronized(同步锁)让其同步,如此一来,某线程调用前必须获取同步锁,调用完后会释放锁给其他线程用,也就是给请求排队,一个接一个按顺序来

java 复制代码
public class Sun {
    private static Sun sun = new Sun();

    public Sun() {} // private constructor

    public static synchronized Sun getInstance(){

        if (null == sun) {
            sun = new Sun(); //沒有sun才构造
        }
        return sun;
    }
}

我们将太阳类Sun中第9行的getInstance()改成了同步方法,如此可避免多线程陷阱。然而这样的做法是要付出一定代价的,试想,线程还没进入方法内部便不管三七二十一直接加锁排队,会造成线程阻塞,资源与时间被白白浪费。我们只是为了实例化一个单例对象而已,犯不上如此兴师动众,使用synchronized让所有请求排队等候。所以,要保证多线程并发下逻辑的正确性,同步锁一定要加得恰到好处,其位置是关键所在

java 复制代码
public class Sun {
    private volatile static Sun sun = new Sun();

    public Sun() {} // private constructor

    public static Sun getInstance(){

        if (null == sun) {
            synchronized (Sun.class) {
                if(null == sun){
                    sun = new Sun(); //沒有sun才构造,只有第一次才构造 保证线程安全
                }
            }

        }
        return sun;
    }
}

我们在太阳类Sun中第3行对sun变量的定义不再使用find关键字,这意味着它不再是常量,而是需要后续赋值的变量;而关键字volatile对静态变量的修饰则能保证变量值在各线程访问时的同步性、唯一性。需要特别注意的是,对于第9行的getInstance()方法,我们去掉了方法上的关键字synchronized,使大家都可以同时进入方法并对其进行开发。请仔细阅读每行代码的注释,有些人(线程)起早就是为了观看日出,那么这些人会通过第10行的判空逻辑进入观日台。而在第11行我们又加上了同步块以防止多个线程进入,这就类似于观日台是一个狭长的走廊,大家排队进入。随后在第12行我们又进行一次判空逻辑,这就意味着只有队伍中的第一个人造了太阳,有幸看到了日出的第一缕阳光,而后面的人则统统离开,直到第17行得到已经造好的太阳

随后发生的事情我们就可以预见了,太阳高高升起,实例化操作完毕,起晚的人们都无须再进入观日台,直接获取太阳实例就可以了,阳光普照大地,将温暖洒向人间。大家注意到没有,我们一共用了2个嵌套的判空逻辑,这就是懒加载模式的"双检锁":外层放宽入口,保证线程并发的高效性;内层加锁同步,保证实例化的单次运行。如此里应外合,不仅达到了单例模式的效果,还完美地保证了构建过程的运行效率,一举两得。

2.4 大道至简

相比"懒汉模式",其实在大多数情况下我们通常会更多地使用"饿汉模式",原因在于这个单例迟早是要被实例化占用内存的,延迟懒加载的意义并不大,加锁解锁反而是一种资源浪费,同步更是会降低CPU的利用率,使用不当的话反而会带来不必要的风险。越简单的包容性越强,而越复杂的反而越容易出错。我们来看单例模式的类结构,如图2-3所示。单例模式的角色定义如下。

■ Singleton(单例):包含一个自己的类实例的属性,并把构造方法用private关键字隐藏起来,对外只提供getInstance()方法以获得这个单例对象。

除了"饿汉"与"懒汉"这2种单例模式,其实还有其他的实现方式。但万变不离其宗,它们统统都是由这2种模式发展、衍生而来的。我们都知道Spring框架中的IoC容器很好地帮我们托管了业务对象,如此我们就不必再亲自动手去实例化这些对象了,而在默认情况下我们使用的正是框架提供的"单例模式"。诚然,究其代码实现当然不止如此简单,但我们应该追本溯源,抓住其本质的部分,理解其核心的设计思想,再针对不同的应用场景做出相应的调整与变动,结合实践举一反三。

相关推荐
捕鲸叉7 小时前
怎样在软件设计中选择使用GOF设计模式
c++·设计模式
啊松同学7 小时前
【Java】设计模式——工厂模式
java·后端·设计模式
捕鲸叉7 小时前
C++设计模式和编程框架两种设计元素的比较与相互关系
开发语言·c++·设计模式
大波V58 小时前
设计模式-参考的雷丰阳老师直播课
java·开发语言·设计模式
编程、小哥哥9 小时前
设计模式之装饰器模式(SSO单点登录功能扩展,增加拦截用户访问方法范围场景)
java·设计模式·装饰器模式
槿花Hibiscus13 小时前
C++基础:Pimpl设计模式的实现
c++·设计模式
吾与谁归in14 小时前
【C#设计模式(4)——构建者模式(Builder Pattern)】
设计模式·c#·建造者模式
shinelord明14 小时前
【再谈设计模式】建造者模式~对象构建的指挥家
开发语言·数据结构·设计模式
matrixlzp19 小时前
Java 责任链模式 减少 if else 实战案例
java·设计模式
编程、小哥哥1 天前
设计模式之组合模式(营销差异化人群发券,决策树引擎搭建场景)
决策树·设计模式·组合模式