GoF23:单例模式(singleton)
单例模式简介
- 核心作用:保证一个类只有一个实例,并且提供一个访问该实例的全局访问点。
- 常见场景:
- Windows的任务管理器
- Windows回收站
- 项目中,读取配置文件的工具类
- 网站的计数器一般也会采用单例模式,可以保证同步
- 数据库连接池的设计一般也是单例模式
- 在Servlet编程中,每个Servlet也是单例的
- 在Spring中,每个Bean默认就是单例的
常见五种单例模式的实现方式
饿汉式
// 饿汉式
public class Hungry {
// 1.私有化构造器
private Hungry() {
}
// 2.类初始化的时候,立即加载该对象
private static Hungry instance = new Hungry();
// 3.提供获取该对象的方法,没有synchronized,效率高
public static Hungry getInstance() {
return instance;
}
}
- 饿汉式是最简单的单例模式的写法,保证了线程的安全,执行效率高,但不能延时加载。
- 但饿汉式也存在一些问题,比如,在单例中要创建大量的数据。
public class Hungry {
private byte[] data1 = new byte[1024];
private byte[] data2 = new byte[1024];
private byte[] data3 = new byte[1024];
private byte[] data4 = new byte[1024];
private Hungry() {
}
private final static Hungry hungry = new Hungry();
public static Hungry getInstance() {
return hungry;
}
}
在Hungry类中,定义了四个byte数组,当代码一运行,这四个数组就被初始化,并且放入到内存中,如果长时间没有使用
getInstance()方法,不需要Hungry类的对象,就会对内存造成一种浪费。我们希望只有在使用
getInstance()方法时,才会去初始化单例类,加载单例类中的数据。因此,有了第二种单例模式:懒汉式。
懒汉式
// 懒汉式
public class LazyMan {
// 1.私有化构造器
private LazyMan() {}
// 2.类初始化的时候,不立即加载该对象
private static LazyMan instance;
// 3.提供获取该对象的方法,有synchronized,效率较低
public static synchronized LazyMan getInstance() {
if (instance == null) {
instance = new LazyMan();
}
return instance;
}
}
- 懒汉式,保证线程安全,可以延时加载,但调用效率不高(获取该实例的方法上加了synchronized)。
DCL懒汉式
public class LazyMan {
private LazyMan() {
}
// 类初始化的时候,不立即加载该对象
private static LazyMan lazyMan;
// 双重检查
public static LazyMan getInstance() {
if (lazyMan == null) {
// 静态同步代码块,锁对象为本类的class属性
/*
加锁是为了确保第一个拿到锁对象的线程创建对象后,
第二个拿到锁对象的线程无需再创建新对象
*/
synchronized (LazyMan.class) {
if (lazyMan == null) {
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
}
- Double Checked Lock 双重锁,优化了懒汉式同步慢的问题,由于JVM底层内部模型原因,偶尔会出现问题,不建议使用
DCL懒汉式的单例,保证了线程的安全性,又符合懒加载,只有在用到的时候,才回去初始化,调用效率也比较高,但是这种写法在极端情况下,还是可能会有一定的问题。
因为
lazyMan = new LazyMan();不是原子操作,至少会经过以下三个步骤:
- 分配内存
- 执行构造方法
- 指向地址
由于指令重排,导致A线程执行
lazyMan = new LazyMan();时,可能先执行了第三步(还没有执行第二步),此时B线程又进来了,发现 lazyMan不为空,直接返回了lazyMan,并且后面使用了返回的 lazyMan,由于此时线程A还没有第二部,导致此时lazyMan还不完整,可能会有一些意象不到的错误,所以就有了下面一种单例模式。这种单例模式只是在上面DCL单例模式增加一个volatile关键字来避免指令重排:
public class LazyMan {
private LazyMan() {
}
// 增加volatile关键字,避免指令的重排,保证它的原子性和一致性
private volatile static LazyMan lazyMan;
// 双重检查
public static LazyMan getInstance() {
if (lazyMan == null) {
// 静态同步代码块,锁对象为本类的class属性
/*
加锁是为了确保第一个拿到锁对象的线程创建对象后,
第二个拿到锁对象的线程无需再创建新对象
*/
synchronized (LazyMan.class) {
if (lazyMan == null) {
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
}
饿汉式改进(静态内部类式)
- 还有这种方式是第一种饿汉式的改进版本,同样也是在类中定义static变量的对象,并且直接初始化,不过是移到了静态内部类中,十分巧妙。既保证了线程的安全性,同时又满足了懒加载。
// 静态内部类实现
public class Holder {
// 1.私有化构造方法
private Holder() {
}
// 2.私有静态内部类
private static class InnerClass {
private static final Holder instance = new Holder();
}
// 3.获取该对象的方法
public static Holder getInstance() {
return InnerClass.instance;
}
}
枚举单例
public enum EnumSingleton {
INSTANCE;
public EnumSingleton getInstance(){
return INSTANCE;
}
}
- 枚举类型是纯天然的单例模式
- 线程安全,调用效率高,但不能延时加载
- 枚举类型是目前最推荐的单例模式的写法,因为足够简单,不需要自己开发去保证线程的安全性,同时可以有效的防止反射来破化我们的单例模式
- 在
newInstance的源码中:

- 如果我们使用反射创建枚举类型对象,就会直接抛出异常
防止反射破坏单例模式
// DCL懒汉式
public class LazyMan {
private LazyMan() {}
// 2.类初始化时,不立即加载该对象
// 增加volatile关键字,避免指令的重排,保证它的原子性和一致性
private volatile static LazyMan instance;
// 3.获取该对象的方法
public static LazyMan getInstance() {
if (instance == null) {
// 静态同步代码块
// 和其他线程竞争本类的锁
synchronized (LazyMan.class) {
if (instance == null) {
instance = new LazyMan();
}
}
}
return instance;
}
}
class LazyManTest {
public static void main(String[] args) throws Exception {
// 调用静态方法创建
LazyMan instance1 = LazyMan.getInstance();
// 使用反射方式创建
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
LazyMan instance2 = declaredConstructor.newInstance();
System.out.println(instance1 == instance2); // false
}
}
问题:当我们调用懒汉模式的静态方法创建了一个单例对象,又使用反射方式创建了一个对象,通过比较得出:两个对象的引用不是同一个引用,这就用反射破坏了我们的单例模式。
解决方式:在私有构造器中进行一个判断,如果 lazyMan 不为空,说明 lazyMan 已经被创建过了,如果正常调用 getInstance 方法,是不会出现这种事情的,所以直接抛出异常
public class LazyMan {
private LazyMan() {
synchronized (LazyMan.class) {
if (lazyMan != null) {
throw new RuntimeException("不要试图用反射破坏单例模式");
}
}
}
private volatile static LazyMan lazyMan;
public static LazyMan getInstance() {
if (lazyMan == null) {
synchronized (LazyMan.class) {
if (lazyMan == null) {
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
}
但是这种写法还是有问题:在上面我们先是正常调用了 getInstance 方法,创建了LazyMan对象,而后第二次用反射创建对象,这样私有构造函数里的判断就起到作用,如果我们两次都是用反射创建对象呢?
问题:如果两次都用反射创建对象,那么还是会创建两个不同的对象,依旧破坏了单例模式。
原因:private volatile static LazyMan lazyMan;这条语句中的lazyMan引用没有被赋值对象的地址,也就是说lazyMan对象没有被构造方法创建,在内存中没有用getInstance方法去初始化lazyMan引用,此时的lazyMan引用一直为null,在私有构造器中的判断就不起作用了。
解决方式:定义一个静态布尔类型的flag变量,初始值为false,私有构造函数里面做一个判断,当第一次创建对象时,flag == false(肯定的),就把 flag 置为 true;当第二次创建对象时,flag已经为ture,这就说明出现问题了,正常的调用是不会第二次跑到私有构造方法中的,所以抛出异常。
public class LazyMan {
private static boolean flag = false;
private LazyMan() {
synchronized (LazyMan.class) {
if (flag == false) {
flag = true;
} else {
throw new RuntimeException("不要试图用反射破坏单例模式");
}
}
}
private volatile static LazyMan lazyMan;
public static LazyMan getInstance() {
if (lazyMan == null) {
synchronized (LazyMan.class) {
if (lazyMan == null) {
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
}
看起来很美好,但是还是不能阻止反射破坏单例模式,因为可以用反射破坏flag的值。
并没有一个很好的方案去避免反射破坏单例模式,但枚举类型是纯天然防止使用反射创建对象的,如果枚举去newInstance就直接抛出异常了。
GoF23:单例模式(singleton)的更多相关文章
- 设计模式之单例模式——Singleton
设计模式之单例模式--Singleton 设计意图: 保证类仅有一个实例,并且可以供应用程序全局使用.为了保证这一点,就需要这个类自己创建自己的对象,并且对外有 ...
- 【白话设计模式四】单例模式(Singleton)
转自:https://my.oschina.net/xianggao/blog/616385 0 系列目录 白话设计模式 工厂模式 单例模式 [白话设计模式一]简单工厂模式(Simple Factor ...
- ooad单例模式-Singleton
单例模式Singleton 主要作用是保证在Java应用程序中,一个类Class只有一个实例存在. 比如建立目录 ...
- iOS单例模式(Singleton)写法简析
单例模式的意思就是只有一个实例.单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例.这个类称为单例类. 1.单例模式的要点: 显然单例模式的要点有三个:一是某个类只能有一个实例: ...
- 浅谈设计模式--单例模式(Singleton Pattern)
题外话:好久没写blog,做知识归纳整理了.本来设计模式就是个坑,各种文章也写烂了.不过,不是自己写的东西,缺少点知识的存在感.目前还没做到光看即能记住,得写.所以准备跳入设计模式这个大坑. 开篇先贡 ...
- 设计模式之——单例模式(Singleton)的常见应用场景
单例模式(Singleton)也叫单态模式,是设计模式中最为简单的一种模式,甚至有些模式大师都不称其为模式,称其为一种实现技巧,因为设计模式讲究对象之间的关系的抽象,而单例模式只有自己一个对象,也因此 ...
- 设计模式之单例模式(Singleton Pattern)
单例模式 单例模式(Singleton Pattern)在java中算是最常用的设计模式之一,主要用于控制控制类实例的数量,防止外部实例化或者修改.单例模式在某些场景下可以提高系统运行效率.实现中的主 ...
- 设计模式(4) -- 单例模式(Singleton)
设计模式(4) -- 单例模式(Singleton) 试想一个读取配置文件的需求,创建完读取类后通过New一个类的实例来读取配置文件的内容,在系统运行期间,系统中会存在很多个该类的实例对象,也就是说 ...
- IOS单例模式(Singleton)
IOS单例模式(Singleton) 单例模式的意思就是只有一个实例.单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例.这个类称为单例类. 1.单例模式的要点: 显然单例模 ...
随机推荐
- intellij idea 设置用真机测试android
android自带的模拟器是不容置疑的慢,genymontion虽然快,但是觉得有点怪的感觉,哈哈,其实这些都不是重点. 之前是用myeclipse开发android的,虽然一直很想用eclipse来 ...
- 常用Linux命令的基本使用
01.常用Linux命令的基本使用 序号 命令 对应英文 作用 01 ls list 查看当前文件夹下的内容 02 pwd print work directory 查看当前所在文件夹 03 cd [ ...
- niuke --abc
链接:https://ac.nowcoder.com/acm/contest/1083/A来源:牛客网 给出一个字符串s,你需要做的是统计s中子串”abc”的个数.子串的定义就是存在任意下标a< ...
- C++枚举算法
枚举算法 什么是枚举? 枚举,顾名思义,就是用最笨的方法,去解决问题(暴力枚举),一个集的枚举是列出某些有穷序列集的所有成员的程序,或者是一种特定类型对象的计数.这两种类型经常(但不总是)重叠. 枚举 ...
- [YII2] Activeform表单部分组件使用方法
文本框:textInput(); 密码框:passwordInput(); 单选框:radio(),radioList(); 复选框:checkbox(),checkboxList(); 下拉框:dr ...
- LABEL和UUID
基本用法 blkid 查看LABEL # blkid -s LABEL /dev/hda3: LABEL="/" /dev/hda1: LABEL="/boot1&quo ...
- BUAA_OO 第一单元总结
1.简单多项式求导 第一次作业的难点,我认为是对输入的预处理,尤其是正则表达式的使用.这次作业的思路是:首先将表达式进行预处理,(由于题目中要求不会有空格产生的WF,所以可以放心大胆的消除空格). 消 ...
- LeetCode 面试题56 - I. 数组中数字出现的次数 | Python
面试题56 - I. 数组中数字出现的次数 题目 一个整型数组 nums 里除两个数字之外,其他数字都出现了两次.请写程序找出这两个只出现一次的数字.要求时间复杂度是O(n),空间复杂度是O(1). ...
- 一篇文章带你编写10种语言HelloWorld
0,编程语言排行榜 计算机编程语言众多,世界上大概有600 多种编程语言,但是流行的也就几十种.我们来看下编程语言排行榜,下面介绍两种语言排行榜. Ⅰ TIOBE 指数 该指数每月更新一次,它监控了近 ...
- Akka 集群单例Cluster Singleton
一.简介 集群中Cluster Singleton 集群中有而只一个单例,可应用于集群全局调控,单一运算决策,中央命名服务或中央路由等应用场景 二.依赖 dependencies { compile ...