介绍
单例模式是设计模式中比较简单容易理解的。它的出现主要是:
保证一个类仅有一个实例,并提供一个访问它的全局访问点
其实就在系统运行期间中保证只有这么一个实例,并能够全局访问。应用场景就是当需要一个对象时,这个对象需要整个系统运行期间只有一个,并且这个对象的新建开销比较大,为了避免频繁的新建对象浪费内存。就使用单例模式。
代码实现
单例模式有比较经典的两种写法。前段时间我去面试的有家公司面试题就有一题,写两种单例的代码实现并分析不同。所以这篇博客也算是面试题分析吧。
懒汉式
懒汉的单例模式重点就在于懒,需要才去创建。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
最简单的懒汉单例模式就上面这写代码,每行代码都有注释说明,但是还要很多问题没有说明清楚,下文再详细分析。
饿汉式
饿汉单例模式代码和上面的差别不大,但是概念上需要引出Java虚拟机的特性,有关static修饰符。
static修饰的变量在类装载时进行初始化。
多个实例的static变量会共享同一块内存区域
分析这两个特性,static变量只会在类装载的时候初始化一次,并且多个实例共享内存区域,非常符合单例的需求。
重复一句话,饿汉的单例模式重点就在于饿,一饿就急就会急着用,没有访问也创建对象。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
饿汉单例模式线程安全。
拓展说明
延迟加载
其实在懒汉单例模式中包含延迟加载的思想,所谓延迟加载就是一开始并不加载资源当有需要才去加载,在实际开发中是很常见的思想,尽可能的节约资源。具体体现就是:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 3
- 4
- 5
- 6
- 7
缓存
懒汉单例模式中还包含了缓存的思想,这也是实际开发中很常见的功能和思想。
简单讲,当有某个资源被频繁使用,并且这些资源都是都在内存外,比如数据库,硬盘文件,网络数据等,每次的读取都是耗时操作频繁访问会造成性能问题。
缓存就是解决这个问题的办法之一,把数据缓存到内存中,每次操作先从内存中寻找资源,如果有就直接使用,没有就获取数据并缓存到内存中方便下次访问。是一种典型的空间换时间的解决方案。
缓存在单例中的实现
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
Android开发中典型的缓存应用场景就是图片缓存,hongyang大神和郭神都有博客介绍使用Goggle提供的LruCache类实现缓存。LruCache使用的就是LRU算法
LRU:Least Recently Used 最近最少使用算法,页面置换算法。
想不起来的同学面壁去吧,大学本科计算机操作系统就学过的内容。
快捷生成单例代码
在Android Studio中提供的快捷生成单例模式的代码就是饿汉模式。在代码文件包右键弹出下图,选中Singleton就能生成单例的代码。
分析
既然有两种单例写法,它们之间存在差别和优缺点就需要分析。
时间和空间
这里指的空间就是内存空间。
- 懒汉式单例是典型的时间换空间,每次取值都要时间做判断,判断是否需要创建实例,当然如果没有外部取值就不会创建对象,节约内存空间。
- 饿汉式单例是典型的空间换时间,类装载时就初始化实例,不管有没有访问取值,不需要做判断节约时间,如果一直没有外部访问取值就浪费了内存空间。
线程安全
1.饿汉式单例是线程安全的,因为虚拟机类加载机制保证了只会创建一次实例,并且装载类的时候不会发生并发。
2.上文的不加同步锁的懒汉式单例是线程不安全的。
下图说明程序运行流程时候AB两个线程访问单例获取方法,注意看时间轴上的方块
可以看到AB线程最后两个时间块在时间轴上部分重合,会创建出两个实例对象来。单例控制失效。
线程安全的懒汉式单例
所以我们需要做的就是分隔时间块,加同步锁就可以实现,不是简单的加锁,而是双重检查加锁
即线程安全又能够性能不受太大的影响。
代码如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
在开源的greenrobot的EventBus中,EventBus类就采用这样的双重检查加锁的单例模式。这里提到EventBus就为为了下文我使用时遇到的坑埋伏笔。
提示:volatile 关键字可能会屏蔽掉虚拟机中一些必要的代码优化,所以运行效率不是很高,一般建议,没有特别需要慎重使用,
更好的单例实现方法
根据上文的分析,常见的两种单例写法都有不同问题和缺陷。最后还有一种写法。即延迟加载又线程安全。这个方案称为 Lazy initialization holder class模式,综合使用Java的类级内部类和多线程缺省同步锁
知识。
类级内部类
- 类级内部类,指有static修饰的成员式内部类,如果没有static修饰的就是对象级内部类
- 类级内部类相当于外部类的static成分,它的对象与外部对象之间不存在依赖关系,因此可以直接创建。而对象级内部类的实例,绑定在外部对象的实例中。
- 类级内部类中,可以定义静态方法,静态方法中只能够引用外部类中的静态成员方法或者成员变量
- 类级内部类相当于其外部类的成员,只在第一次使用时才会被装载。
多线程缺省同步锁
在多线程开发中除了使用synchronized同步锁实现同步控制之外,虚拟机有一些隐含的同步控制,这样就不用我们控制同步,这些隐含的情况包括
- 由静态初始化器(静态字段或static{}代码块的初始化器)初始化数据时
- 访问final字段时
- 在创建线程之前创建对象时
- 线程可以看见它将要处理的对象时
实现代码
实现的代码思路就是,用静态初始化器的方式,由虚拟机保证线程安全。用类级内部类负责创建实例,只要不使用到这个类级内部类就不会创建实例。两者结合就实现了延迟加载和线程安全。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
上面的代码在运行中,当getInstance方法第一次被调用,它第一次读取SingletonHolder.instance导致Singleton类得到初始化,而这个类在装载时并同时被初始化,会初始化它的静态域,从而创建Singleton实例,因为是静态域,因此只会在虚拟机装载类的时候初始化一次,由虚拟机保证它的线程安全。这样写的优势就是,getInstance方法没有被同步加锁,并且只是执行一个域的访问,延迟加载没有增加任何的访问开销。
枚举单例
以为我们已经找到很好单例写法和问题解决办法。但是Java就是这么有意思,总有新的想法提出和问题出现。
前文的双重检查加锁(double checked locking)
单例在Java1.5之前也会某种情况下产生多个实例,并且volatile关键字也会导致的一些复杂的问题
在《Effective Java 》第二版第3条中,提到这一句话
单元素的枚举类型已经成为实现单例的最佳方法
这是在Java1.5发行版之后,枚举能够实现单例,只需要编写一个包含单个元素的枚举就可以实现线程安全代码简单的单例。
这里需要说明枚举
- Java的枚举类型实质上功能齐全的类,它有自己的属性和方法
- Java枚举类型的基本思想是通过公有的静态final域为每个枚举常量导出实例的类
-
从某个角度上看,枚举是单例的泛型化,本质上是单元素的枚举
代码
概念这么多,代码其实就几行。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
这种单例在功能上与公有域方法相近,但是代码简洁,由虚拟机提供序列化机制,绝对防止反射等方法导致的多次实例化。简洁。高效、线程安全,真的可以说是最佳单例写法。
在使用的时候
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
总结
- 全文详细说明两个单例模式的写法和优缺点,以及各自的不同。
- 在详细分析之后,再延伸出三个单例的进化版双重检查加锁和类级内部类单例以及枚举单例。都有详细的注释说明。
- 作为需要了解学习单例的写法和分析已经足够,但是实际开发中会遇到更复杂的情况,我下篇博客再分析说明。