精雕细琢——全方位解析单例模式

单例模式有的时候特别重要,因为某些系统是要求某个类在整个生命周期中有且只有一个实例存在,这时候就要用到单例模式。

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

单例模式也是创建型设计模式。

我们一步步研究。

按照惯例,先讲故事。

各个大学想请史上最牛科学家来自己学校讲课。

分析一波,既然是史上最牛科学家,那么就代表只有一个人,因为并没有说“之一”二字,带了“之一”的话那就是一批人。所以这个人假设就叫Edward,就是史上最牛科学家的唯一实例。各个大学现在要来抢他,不可能把他拆了,必须按照人家的档期来安排。

那么到底如何请呢?

请史上最牛科学家套路第一版

private static BestScientist edward = null;

private BestScientist(){}

public static BestScientist getInstance(){
    if(edward == null){
        edward = new BestScientist();
    }
    return edward;
}

我们观察,上面单例模式的定义说得清楚,只提供一个访问它的全局访问点,那么其他内部的东西都要被封装。所以edward作为静态变量是private的。只留了一个getInstance()方法作为全局访问点。

要注意,把构造函数重写改为private,将外部直接创建实例的机会堵住。

为什么是static?

因为只有变量定义为static,它才是属于类的,而不是属于某个实例,它在类加载的时候就能被初始化,这个变量才是唯一的。

而方法只有是静态的,才可以直接使用类去调用而不必先创建实例。

BestScientist.getInstance();// Edward就来了。

缺陷:

不能满足并发请求,当一个线程进来的时候刚判断完Edward是空,这时候另一个线程却刚刚完成创建,那么第一个线程因为已经过了空值判断的约束,就会再创建一个实例。那就乱了,相当于出来了一个Edward的克隆人。

请史上最牛科学家套路第二版

private static BestScientist edward = new BestScientist();

private BestScientist(){}

public static BestScientist getInstance(){
    return edward;
}

这个版本也叫饿汉版,就是着急,类加载的时候就把唯一实例创建完了,其他人来用的时候就过来取就行了,系统永远不会出现多个实例。

请史上最牛科学家套路第三版

private static BestScientist edward = null;

private BestScientist(){}

synchronized public static BestScientist getInstance(){
    if(edward == null){
        edward = new BestScientist();
    }
    return edward;
}

也叫懒汉式,就是不着急,实例的创建被延迟到使用的时候,正如我们在浏览器中滑到那个位置才开始加载一样,这样可以减少系统负担。这基本就是第一版的并发版本,加了把锁强制多线程时单个线程获得锁独享操作,其他线程等待。锁的位置也可以移到方法体内部。

private static BestScientist edward = null;

private BestScientist(){}

public static BestScientist getInstance(){
    if(edward == null){
        synchronized(BestScientist.class){
            edward = new BestScientist();
        }
    }
    return edward;
}

貌似更精确,但是问题又出现了,当一个线程获得锁开始创建实例的时候,另外好几个线程都经过了null判断阻塞在马上创建实例的前面,一旦锁被释放,将创建多个实例出来。

private volatile static BestScientist edward = null;

private BestScientist(){}

public static BestScientist getInstance(){
    if(edward == null){
        synchronized(BestScientist.class){
            if(edward == null){
                edward = new BestScientist();
            }
        }
    }
    return edward;
}

锁内再加一层null判断即可解决上面的问题。这就是Double-check Locking。懒汉式的最终版本。

什么是volatile?

volatile关键字是多线程编程中常常用到的,他的效果与锁很类似,但是实现方式却有不同。这里简单介绍一下计算机内存的结构,计算机内存结构分为堆栈,堆(heap)是计算机主内存,而栈(stack)多是一些变量,日常我们使用的时候,都是直接操作这个变量,变量会在被操作结束以后写回主内存。而多线程系统在这种方式下就会出现问题,因为变量在被一个线程未操作完之前,被另一个或多个线程也做了修改,那么就看谁是最后一个修改的,最终只有这一个修改被写回主内存,造成其他线程写入的修改无效的bug。这时候,使用volatile关键字修饰该变量,会让该变量的每次修改都写回主内存,其他线程均等待其修改完成以后再写入,以保证变量被操作时的同步性。

请史上最牛科学家套路第四版

我们发现饿汉式和懒汉式各有利弊,饿汉式非常简单,但是占用了系统资源,而懒汉式虽然可以做到用的时候再创建,但是需要很多判断,还有锁机制,会影响性能。那么下面将使用的是饿汉懒汉合二为一,克服了两者各自的缺点,集成了两者的优点,那就是IoDH(在军需官那里初始化),军需官就是ClassHolder。

private BestScientist(){}

private static class ClassHolder{
    private ClassHoder(){
        private final static BestScientist edward = new BestScientist();
    }
}

public static BestScientist getInstance(){
    return ClassHolder.edward;
}

外部仍旧是通过

BestScientist.getInstance();// Edward就来了。

获取实例,但是内部实现却大不一样。这里去掉了锁,增加了一个static class,这个类的构造函数里定义了一个静态变量并初始化创建实例。最终效果是,当BestScientist类加载时,其静态内部类ClassHolder并没有任何动作,ClassHolder是什么时候加载的呢?是在getInstance方法中获取ClassHolder内部静态变量的时候加载,而随着ClassHolder被加载,其静态变量会跟着一起初始化。这可以称得上完美,尽取以上饿汉懒汉优势而摒弃双方劣势。

为什么是static class?

static关键字有两个特性,上面已经提到,这里再重申一次

  • static定义的变量属于类,可以直接类名调用。
  • static修饰的内容随着类加载就会被运行。

static所有针对的内容都是类,它是属于类的而不是实例。所以,反过来说,类被加载的时候就会同时运行static修饰的内容,类加载是在类被使用的时候,使用完了以后会按照GC机制清除。


扩展

  • 单例模式限制了内存中仅存一个实例,外部仅通过一个公共访问点使用。降低了内存的消耗,支撑了业务中一些必要的需求。
  • 可以稍作改造,根据业务需要以及机器性能,将内存中的实例个数上升到两个或者一个固定的个数,这样在某个单例被过度使用的时候,可以被分担一些压力。

问题

有些人可能觉得单例类的职责过重,既有工厂角色又有产品角色,但我不这么认为,单例类的内部结构在IoDH就趋于非常稳定的状态,不会有什么扩展,所以不必去增加抽象层,而它的职责就是提供唯一实例,这是个高内聚的类,所以我认为并不违反“单一职责原则”,就看你在哪一个层面看问题了。

注意

上面提到了类和类的实例在使用完毕以后,会按照GC机制去清除,那么可能会造成共享实例被清除的后果。但是GC默认的机制是按照使用计数器,当这个实例没有任何线程去使用的时候,它的清除级别也是较低的,即使被清除了,再在使用的时候去创建也未尝不可,这比起其他版本的开销来说已经非常高效。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏菩提树下的杨过

oracle:wm_concat函数与oracle版本

oracle中有一个看似很NB的内置函数wm_concat,可以方便的实现“行转列”功能(相关用法,大家自行搜索一下,能找到很多资料) 今天偶然发现一个问题: ...

2216
来自专栏Laoqi's Linux运维专列

shell脚本 + date命令语法

74711
来自专栏编程

java基础思维图解

Java是一门面向对象编程语言,不仅吸收了C++语言的各种优点,还摒弃了C++里难以理解的多继承、指针等概念,因此Java语言具有功能强大和简单易用两个特征。J...

2079
来自专栏数据和云

巧用SQL:Oracle中实现split相关方法总结

尚世波 从事数据库方面工作多年,专注于pl/sql开发、数据库设计、优化方面的研究,喜欢挑战 前文回顾:巧用SQL:oracle pl/sql split函...

3845
来自专栏学习力

《Java从入门到放弃》框架入门篇:Struts2的常用验证方式

2048
来自专栏编程

C语言嵌入式系统编程修炼之性能优化

这是我13年前创作和发表在互联网上的文章,这么多年过去了,这篇文章仍然在到处传播。现在贴回Linuxer公众号。 全文目录: C语言嵌入式系统编程修炼之道——背...

2277
来自专栏北京马哥教育

Redis 数据结构使用场景

一、redis 数据结构使用场景   原来看过 redisbook 这本书,对 redis 的基本功能都已经熟悉了,从上周开始看 redis 的源码。目前目标...

3854
来自专栏指尖下的Android

由单例模式的双判空所展开的思考

相信很多朋友对于单例模式都很熟悉,一般常见的就七八种,百度一大堆,这里聊一下双判空情况下的单例模式。 双判空单例是由单判空所演变而来的,是原来的一些程序员为了...

831
来自专栏佳爷的后花媛

h文件和c文件的区别include本身只是一个简单的文件包含预处理命令,即为把include的后面文件放到这条命令这里,除此之外,没有其它的用处(至少我也样认为).

其实在H文件里写函数也无所谓,只是不符合习惯而已。只要按照以上的格式写,一个H文件添加多少次都无所谓,

2002
来自专栏惨绿少年

Shell编程基础篇-上

1.1 前言 1.1.1 为什么学Shell Shell脚本语言是实现Linux/UNIX系统管理及自动化运维所必备的重要工具, Linux/UNIX系统的底层...

2000

扫码关注云+社区