关键词:
目录
一、概念
单例模式是运用最广泛的设计模式之一,在应用这个模式时,单例模式的类必须保证只有一个实例存在。多用于整个程序只需要有一个实例,通常很消耗资源的类,比如线程池,缓存,网络请求,IO操作,访问数据库等。由于类比较耗资源,所以没必要让它构造多个实例,这种就是单例模式比较好的使用场景。
1.1 单例类
单例模式(Singleton Pattern):一个类有且仅有一个实例,并且自行实例化向整个系统提供,也称为单例类。
单例模式有三个要点:
-
1.某个类只能有一个实例。
-
2.必须自行创建这个实例。
-
3.必须给所有其他对象提供这一实例。
具体实现角度来说就是以下几点:
-
1.单例模式的类只提供私有的构造函数。
-
2.通过一个静态方法或者枚举返回单例类对象。
-
3.确保单例类有且只有一个静态私有对象,尤其是在多线程环境下。
-
4.提供了一个静态的公有的函数用于创建或获取它本身的静态私有对象。
-
5.确保单例类对象在反序列化时不会重新构建对象。
在单例类的内部实现只生成一个实例,同时它提供一个静态的getInstance()工厂方法,让客户可以访问它的唯一实例;为了防止在外部对其实例化,将其构造函数设计为私有;在单例类内部定义了一个Singleton类型的静态对象,作为外部共享的唯一实例。
1.2 优缺点
1.2.1 优点
-
1.单例模式提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。
-
2.由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。
-
3.允许可变数目的实例。基于单例模式我们可以进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例,既节省系统资源,又解决了单例单例对象共享过多有损性能的问题。
1.2.2 缺点
-
1.由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
-
2.单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。
-
3.现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失。
二、创建单例模式的方法
2.1 饿汉式
强调饿,那么在创建对象实例的时候就比较着急,饿了嘛,于是在装载类的时候就创建对象实例。
这种方法非常简单,因为单例的实例被声明成 static 和 final 变量,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。
public class SingletonHungry
//类加载时就初始化
private static final SingletonHungry singleton = new SingletonHungry();
private SingletonHungry()
public static SingletonHungry getInstance()
return singleton;
缺点是它不是一种懒加载模式,即使客户端没有调用 getInstance()方法,单例会在加载类后一开始就被初始化。
饿汉式的创建方式在一些场景中将无法使用:如 Singleton 实例的创建是依赖参数或者配置文件的,在 getInstance() 之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用了。
2.2 懒汉式
强调懒,那么在创建对象实例的时候就不着急,什么时候用什么时候创建。所以在装载对象的时候不创建对象实例。
2.2.1 懒汉式(非线程安全)
public class SingletonLazy
private static SingletonLazy singletonLazy;
private SingletonLazy()
public static SingletonLazy getInstance()
if (singletonLazy == null)
singletonLazy = new SingletonLazy();
return singletonLazy;
这段代使用了懒加载模式,但是却存在致命的问题。当有多个线程并行调用 getInstance() 的时候,就会创建多个实例。也就是说在多线程下不能正常工作。那么怎么解决?最简单的方法是给 getInstance() 方法加个同步锁(synchronized)。
2.2.2 懒汉式(线程安全)
public class SingletonLazy
private static SingletonLazy singletonLazy;
private SingletonLazy()
public static synchronized SingletonLazy getInstance()
if (singletonLazy == null)
singletonLazy = new SingletonLazy();
return singletonLazy;
上面通过添加 synchronized 关键字,使得getInstance()是一个同步方法,保证多线程情况下单例对象的唯一性。
虽然做到了线程安全,并且解决了多实例的问题,但是它并不高效。因为在任何时候只能有一个线程调用 getInstance() 方法。但是同步操作只需要在第一次调用时才被需要,即第一次创建单例实例对象时。这就引出了双重检验锁。
2.3 双重检验锁
双重检验锁模式(double checked locking pattern),是一种使用同步块加锁的方法。程序员称其为双重检查锁,也是网上使用毕竟频繁的一种方式。
为什么叫双重检查锁?因为会有两次检查 instance == null,一次是在同步块外,一次是在同步块内。为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,避免不必要的同步。如果在同步块内不进行二次检验的话就会生成多个实例,避免生成多个实例。
public class SingletonDCL
private static SingletonDCL singleton;
private SingletonDCL()
public static SingletonDCL getInstance()
if (singleton == null)
synchronized (SingletonDCL.class)
if (singleton == null)
singleton = new SingletonDCL();
return singleton;
这段代码看起来很完美有着双重检查,但很可惜,它是有问题。主要在于 singleton = new SingletonDCL()。事实上在 JVM 中这句话大概做了下面 3 件事情:
-
1.给 singleton 分配内存。
-
2.调用 SingletonDCL 的构造函数来初始化成员变量。
-
3.将singleton对象指向分配的内存空间(执行完这步 singleton 就为非 null)。
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第2步和第3步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、但 2 未执行之前,被线程二抢占了,这时 singleton 已经是非 null 了(但却没有初始化),所以线程二会直接返回 singleton(第2步未执行),然后使用,然后顺理成章地报错。
我们只需要将 singleton 变量声明成 volatile 就可以了。
public class SingletonDCL
private volatile static SingletonDCL singleton;//变量声明成volatile
private SingletonDCL()
public static SingletonDCL getInstance()
if (singleton == null)
synchronized (SingletonDCL.class)
if (singleton == null)
singleton = new SingletonDCL();
return singleton;
使用 volatile 的主要原因是其有一个特性:禁止指令重排序优化。也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。比如上面的例子,取操作必须在执行完 1-2-3 之后或者 1-3-2 之后,不存在执行到 1-3 然后取到值的情况。
当然 volatile 变量还有一个规则:对一个变量的写操作先行发生于后面对这个变量的读操作(这里的"后面"是时间上的先后顺序)。
2.4 静态内部类
public class SingletonNested
//静态内部类
private static class SingletonHolder
private static final SingletonNested singleton = new SingletonNested();
private SingletonNested()
public static SingletonNested getInstance()
return SingletonHolder.singleton;
使用JVM本身机制保证了线程安全问题。由于静态单例对象没有作为Singleton的成员变量直接实例化,因此类加载时不会实例化SingletonNested,第一次调用getInstance()时将加载内部类SingletonHolder,在该内部类中定义了一个static类型的变量singleton,此时会首先初始化这个成员变量,由Java虚拟机来保证其线程安全性,确保该成员变量只能初始化一次。由于getInstance()方法没有任何线程锁定,因此其性能不会造成任何影响。 s
由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的,同时读取实例的时候不会进行同步,没有性能缺陷,也不依赖 JDK 版本。
2.5 枚举
public enum SingletonEnum
SINGLETON;
public void doSomeThing()
我们可以通过SingletonEnum.SINGLETON来访问实例,这比调用getInstance()方法简单多了。创建枚举默认就是线程安全的,所以不需要担心double checked locking,而且还能防止反序列化导致重新创建新的对象。
小结
单例模式不管用那种方式实现,核心思想都相同:
-
1.构造函数私有化,通过一次静态方法获取一个唯一实例
-
2.线程安全
使用场景:
-
1.需要频繁的进行创建和销毁的对象。
-
2.创建对象时耗时过多或耗费资源过多,但又经常用到的对象。
-
3.工具类对象。
-
4.频繁访问数据库或文件的对象。
一般情况下直接使用饿汉式就好了,当然推荐使用文中DCL方式和静态内部类的方式来创建单例模式。如果涉及到反序列化创建对象时会试着使用枚举的方式来实现单例。当然,枚举单例的优点就是简单,但是大部分应用开发很少用枚举,可读性并不是很高,不建议用。
三、扩展
3.1 防止反序列化
上文使用枚举可以防止反序列化导致重新创建新的对象。那么其他几种实现单例模式的方式怎么方式防止反序列化导致重新创建新的对象?那就是反序列化。可以参考序列化一文。
反序列化操作提供了一个很特别的钩子函数,类中具有一个私有的readResolve()函数,这个函数可以让开发人员控制对象的反序列化。
public class SingletonDCL implements Serializable
private volatile static SingletonDCL singleton;//变量声明成volatile
...
private Object readResolve() throws ObjectStreamException
return singleton;
在readResolve方法中将单例对象返回,而不是重新生成一个新对象。
3.2 volatile 关键字
Java内存模型规定了所有的变量都存储在主内存中。每条线程中还有自己的工作内存,线程的工作内存中保存了被该线程所使用到的变量(这些变量是从主内存中拷贝而来)。线程对变量的所有操作(读取,赋值)都必须在工作内存中进行。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
作用
-
1.线程可见性
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
-
2.指令重排序
没加之前,指令是并发执行的,第一个线程执行到一半另一个线程可能开始执行了。加了volatile关键字后,不同线程是按照顺序一步一步执行的。例如上面2.3 双重检验锁。
android单例模式必知必会(代码片段)
目录一、概念1.1单例类1.2优缺点1.2.1优点1.2.2缺点二、创建单例模式的方法2.1饿汉式2.2懒汉式2.2.1懒汉式(非线程安全)2.2.2懒汉式(线程安全)2.3双重检验锁2.4静态内部类2.5枚举小结三、扩展3.1防止反序列化3.2volatile关键字一、概念 ... 查看详情
设计模式必知必会系列终章(代码片段)
目录装饰器模式工厂方法模式抽象工厂模式编辑适配器模式代理模式装饰器模式官方定义: 动态地给⼀个对象增加⼀些额外的职责。就增加功能而言,装饰器模式比生成子类更为灵活。——《设计模式》GoF通俗解释: 装... 查看详情
springmvc--必知必会(代码片段)
...SpringMVC基于模型--视图--控制器(Model-View-Controller,MVC)模式实现,属于SpringFrameWork的后续产品,已经融合在SpringWebFlow里面。它通过一套注解,让一个简单的Java类成为处理请求的控制器,而无需实现任何接口。同时它还支持RESTfu... 查看详情
scala必知必会(代码片段)
...伴生对象case和trait集合数组ListSetMapOptuon&Some&NoneTuple模式匹配基本类型List类型匹配异常处理高级函数字符串匿名函数Curry 查看详情
android必知必会-app常用图标尺寸规范汇总(代码片段)
1.程序启动图标(iconlauncher) 放在mipmap-*dpi下,文件名为ic_launcher.pngL DPI(LowDensityScreen,120DPI),其图标大小为36x36pxM DPI(MediumDensityScreen,160DPI),其图标大小为48x48pxH DPI(HighDensityScr 查看详情
android必知必会-rgba转argb(代码片段)
...下发的颜色值字符串由于一开始依据iOS端的RGBA格式,Android端(Android使用ARGB方式)需要进行兼容,需要对此字符串转换。举例:RGBA#ABCDEF99=>ARGB#99ABCDEF方式①字符串截取和组合Stringargb 查看详情
android必知必会-recyclerview恢复上次滚动位置(代码片段)
如果移动端访问不佳,请访问–>Github版记录RecyclerView滚动位置并恢复是一个很常见的需求,通常需要精准恢复到上次的位置。预计会用到RecyclerView相关的三个知识点:监听RecyclerView滚动状态监听RecyclerView完成绘制... 查看详情
mysql学习--mysql必知必会(代码片段)
?上图为数据库操作分类:??下面的操作參考(mysql必知必会)创建数据库运行脚本建表:mysql>createdatabasemytest;QueryOK,1rowaffected(0.07sec)mysql>showdatabases;+--------------------+|Database|+--------------------+|infor 查看详情
正则表达式必知必会(代码片段)
基本概念正则表达式描述了一种字符串匹配的文字模式,由普通字符(例如字符a到z)以及特殊字符(称为元字符)组成,将该模式与所搜索的字符串进行匹配。通俗的讲,正则表达式相当于定义了一个模板,从某个字符串中按... 查看详情
android必知必会-app常用图标尺寸规范汇总(代码片段)
若移动端访问不佳,请使用–>Github版内容持续更新中,更新日期:2016-08-111.程序启动图标(iconlauncher)放在mipmap-*dpi下,文件名为ic_launcher.pngLDPI(LowDensityScreen,120DPI),其图标大小为36x36 查看详情
hive必知必会(代码片段)
hive: 基于hadoop,数据仓库软件,用作OLAPOLAP:onlineanalyzeprocess 在线分析处理OLTP:onlinetransactionprocess在线事务处理 事务: ACID A:atomic 原子性 C:consistent 一致性 I:isolation 隔离性 D:durability 持久性 1读未提交 脏读 //事务... 查看详情
android必知必会-极简版leancloud短信验证码功能(代码片段)
如果移动端访问不佳,请访问==>Github版使用LeanCloud的RESTAPI来自定义短信验证码相关功能,不再需要臃肿的SDK。背景公司的项目仅仅使用了Leancloud短信功能来发送验证码,刚开始Leancloud的短信SDK还会和项目中的... 查看详情
android开发必知必会:java线程池(代码片段)
池化技术(Pool)池化技术(Pool)是一种很常见的编程技巧,我们日常工作中常见的有数据库连接池、线程池、对象池等,它们的特点都是将“昂贵的”、“费时的”的资源维护在一个特定的“池子”中,规定其... 查看详情
移动appium测试必知必会(代码片段)
针对移动端Android的测试,adb命令是很重要的一个点,必须将常用的adb命令熟记于心,将会为Android测试带来很大的方便,其中很多命令将会用于自动化测试的脚本当中. ADB,中文名安卓调试桥,它是... 查看详情
mysql必知必会(代码片段)
姊妹篇——Hive必知必会(数据仓库):https://hiszm.blog.csdn.net/article/details/119907136文章目录第一章:数据库基础基本概念什么是SQL第二章:MySQL简介第三章:了解数据库和表第四章:检索数据SELECT语句第五章:... 查看详情
crypto必知必会(代码片段)
crypto必知必会最近参加了个ctf比赛,在i春秋,南邮方面刷了一些crypto密码学题目,从中也增长了不少知识,在此关于常见的密码学知识做个小总结!Base编码Base编码中用的比较多的是base64,首先就说一下Base64编码方式将字符串以... 查看详情
androidservice完全解析之必知必会(代码片段)
想必对于Android开发者来说,对Service一定不陌生了,作为大名鼎鼎的四大组件之一的service,在Android中有着不可替代的作用,它不像Activity那么光鲜亮丽,一般都是默默躲在后台执行着一些“见不得人的”任务... 查看详情
h5系列之history(必知必会)(代码片段)
H5系列之History(必知必会)目录概念兼容性属性方法H5方法概念理解HistoryApi的使用方式目的是为了解决哪些问题作用:ajax获取数据时,可以改变历史记录,从而可以使用浏览器的后退和前进。【】规范地址:http://www.w3.org/TR/html5... 查看详情