文章很长,而且持续更新,建议收藏起来,慢慢读!疯狂创客圈总目录 博客园版 为您奉上珍贵的学习资源 :
免费赠送 :《尼恩Java面试宝典》 持续更新+ 史上最全 + 面试必备 2000页+ 面试必备 + 大厂必备 +涨薪必备
免费赠送 经典图书:《Java高并发核心编程(卷1)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷2)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷3)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《尼恩Java面试宝典 最新版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 资源宝库: Java 必备 百度网盘资源大合集 价值>10000元 加尼恩领取
该如何优雅的、安全的使用单例模式呢?
单例模式是Java的核心模式,最好人人都精通。
那么,该如何优雅的使用单例模式呢?
来看看:
- 缓存之王 Caffeine 源码中,如何使用单例模式的?
- 链路之王 Skywalking 源码中,如何使用单例模式的?
另外,也看看 美团是如何进行 单例模式 的面试的。下面是一个美团面试题:
- 单例模式懒汉式和饿汉式有哪些区别?(美团)
注:本文以 PDF 持续更新,最新尼恩 架构笔记、面试题 的PDF文件,请从下面的链接获取:语雀 或者 码云
1.什么是单例
- 保证一个类只有一个实例,并且提供一个访问该全局访问点
2.那些地方用到了单例模式
- 网站的计数器,一般也是采用单例模式实现,否则难以同步。
- 应用程序的日志应用,一般都是单例模式实现,只有一个实例去操作才好,否则内容不好追加显示。
- 多线程的线程池的设计一般也是采用单例模式,因为线程池要方便对池中的线程进行控制
- Windows的(任务管理器)就是很典型的单例模式,他不能打开俩个
- windows的(回收站)也是典型的单例应用。在整个系统运行过程中,回收站只维护一个实例。
3.单例优缺点
优点:
- 在单例模式中,活动的单例只有一个实例,对单例类的所有实例化得到的都是相同的一个实例。这样就防止其它对象对自己的实例化,确保所有的对象都访问一个实例
- 单例模式具有一定的伸缩性,类自己来控制实例化进程,类就在改变实例化进程上有相应的伸缩性。
- 提供了对唯一实例的受控访问。
- 由于在系统内存中只存在一个对象,因此可以节约系统资源,当需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能。
- 允许可变数目的实例。
- 避免对共享资源的多重占用。
缺点:
- 不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。
- 由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。
- 单例类的职责过重,在一定程度上违背了“单一职责原则”。
- 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。
4.单例模式使用注意事项:
- 使用时不能用反射模式创建单例,否则会实例化一个新的对象
- 使用懒单例模式时注意线程安全问题
- 饿单例模式和懒单例模式构造方法都是私有的,因而是不能被继承的,有些单例模式可以被继承(如登记式模式)
5.单例防止反射漏洞攻击
private static boolean flag = false;
private Singleton() {
if (flag == false) {
flag = !flag;
} else {
throw new RuntimeException("单例模式被侵犯!");
}
}
public static void main(String[] args) {
}
6.如何选择单例创建方式
- 如果不需要延迟加载单例,可以使用枚举或者饿汉式,相对来说枚举性好于饿汉式。
如果需要延迟加载,可以使用静态内部类或者懒汉式,相对来说静态内部类好于懒韩式。
最好使用饿汉式
7.单例创建方式
(主要使用懒汉和懒汉式)
1.饿汉式:
类初始化时,会立即加载该对象,线程天生安全,调用效率高。
2.懒汉式:
类初始化时,不会初始化该对象,真正需要使用的时候才会创建该对象,具备懒加载功能。
3.静态内部方式:
结合了懒汉式和饿汉式各自的优点,真正需要对象的时候才会加载,加载类是线程安全的。
4.枚举单例:
使用枚举实现单例模式
优点: 实现简单、调用效率高,枚举本身就是单例, 由jvm从根本上提供保障!避免通过反射和反序列化的漏洞;
缺点: 没有延迟加载。
5.双重检测锁方式
因为JVM重排序、内存可见性的原因,可能会初始化多次,
所以: 需要通过 Double Check 双重检查+ synchronized + Volatile 解决 同步问题和可见性问题。
1.饿汉式
类初始化时,会立即加载该对象,线程天生安全,调用效率高。
package com.crazymakercircle.designmodel.singleton;
//饿汉式
public class FSingleton {
// 类初始化时,会立即加载该对象,线程安全,调用效率高
private static final FSingleton instance = new FSingleton();
// 私有化构造方法
private FSingleton() {
}
public static FSingleton getInstance() {
return instance;
}
}
饿汉模式就是类一旦加载,就把单例初始化完成,保证getInstance的时候,单例是已经存在的了。
特点:
- 是否 Lazy 初始化:否
- 是否多线程安全:是
- 实现难度:易
优点:
- 没有加锁,执行效率会提高。
- 这种方式比较常用,但容易产生垃圾对象
- 它基于JVM class loader 机制, 是单线程执行的, 避免了多线程的同步问题
缺点:
- 类加载时就初始化,浪费内存,
2.懒汉式
类初始化时,不会初始化该对象,
真正需要使用的时候,才会创建该对象,具备懒加载功能。
package com.crazymakercircle.designmodel.singleton;
//懒汉模式
public class FLazySingleton {
//类初始化时,不会初始化该对象,真正需要使用的时候才会创建该对象。
private static FLazySingleton instance = null;
// 私有化构造方法
private FLazySingleton() {
}
//真正需要使用的时候才会创建该对象
public static synchronized FLazySingleton getInstance() {
if(null==instance)
{
instance=new FLazySingleton();
}
return instance;
}
}
3.静态内部类
静态内部方式:
结合了懒汉式和饿汉式各自的优点,真正需要对象的时候才会加载,加载类是线程安全的。
package com.crazymakercircle.designmodel.singleton;
public class Singleton {
//静态内部类
private static class LazyHolder {
//通过final保障初始化时的线程安全
private static final Singleton INSTANCE = new Singleton();
}
//私有的构造器
private Singleton (){}
//获取单例的方法
public static final Singleton getInstance() {
//返回内部类的静态、最终成员
return LazyHolder.INSTANCE;
}
}
4.枚举单例式
枚举单例:
使用枚举实现单例模式 优点:实现简单、调用效率高,
枚举本身就是单例,由jvm从根本上提供保障!避免通过反射和反序列化的漏洞, 缺点没有延迟加载。
package com.lijie;
package com.crazymakercircle.designmodel.singleton;
//饿汉式
public enum SingletonEnumStyle {
INSTANCE;
// 类初始化时,会立即加载该对象,线程安全,调用效率高
public static SingletonEnumStyle getInstance() {
return INSTANCE;
}
}
枚举实现单例模式 优点:
- 实现简单、枚举本身就是单例,由jvm从根本上提供保障!
- 避免通过反射和反序列化的漏洞
缺点:
- 没有延迟加载
5.双重检测锁方式
所谓懒加载,就是直到第一次被调用时才加载。其实现需要考虑并发问题和指令重排,代码如下:
public class Singleton {
private volatile static Singleton instance; //①
private Singleton() { //②
}
public static Singleton getInstance() {
if (instance == null) {//③
synchronized (Singleton.class) {
if (instance == null) {//④
instance = new Singleton();//⑤
}
}
}
return instance;
}
}
这段代码精简至极,没有一个字符是多余的,下面逐行解读一下:
首先,注意到①处的volatile关键字,它具备两项特性:
一是保证此变量对于所有线程的可见性。
即当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。
二是禁止指令重排序优化。
这里解释一下指令重排序优化:
代码 ⑤ 处的instance = new Singleton(); 并不是原子的,大体可分为如下 3 步:
- 分配内存
- 调用构造函数初始化成实例
- 让instance指向分配的内存空间
JVM 允许在保证结果正确的前提下进行指令重排序优化。
即如上 3 步可能的顺序为1->2->3 或 1->3->2 。
如果顺序是 1->3->2 ,当 3 执行完,2 还未执行时,另一个线程执行到代码 ③ 处,发现instance不为null,直接返回还未初始化好的instance并使用,就会报错。
所以使用volatile,就是为了保证线程间的可见性和防止指令重排。
其次,代码②处将构造函数声明为private目的:在于阻止使用new Singleton()这样的代码生成新实例。
最后,当客户端调用Singleton.getInstance()时,先检查是否已经实例化(代码③),未实例化时同步代码块,然后再次检查是否已实例化(代码④),然后才执行代码⑤。
两次检查的意义在于,防止synchronized同步过程中其他线程进行了实例化。
这就是著名的双重检查锁(Double check lock)实现单例,也即懒加载。
TIPS:
网上也有直接对 getInstance()方法加锁的版本,这样大范围的方法级别加锁会导致并发变低,实际上第一次调用生成实例之后,后续获取实例根本不需要并发控制了。
而本例的双重检查锁版本可以避免此并发问题。
双重检测锁 单例 非常重要, 涉及到Volatile 和可见性的底层原理, 深入学习/系统学习 双重检测锁 单例的内容, 请参见 《Java 高并发核心编程 卷2》 第8.1节:线程安全的单例模式
缓存之王 Caffeine 源码中,如何使用单例模式的?
答案是:枚举单例
并且,单例的名称叫做 INSTANCE
通过这个 INSTANCE 名字 做 关键词搜索, 能搜到一大把
来一个案例
再来一个案例
缓存之王 Caffeine 的详细资料,请参考下面的博客、或者对应的PDF文件:
- 《彻底穿透 缓存之王:Caffeine 源码、架构、原理(史上最全,10W字 超级长文)》
- 《彻底穿透 缓存之王:Caffeine 的使用(史上最全)》
链路之王 Skywalking 源码中,如何使用单例模式的?
答案是:枚举单例
并且,单例的名称叫做 INSTANCE
8 单例模式懒汉式和饿汉式有哪些区别?(美团)
单例模式是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
明确定义后,看一下代码:
饿汉模式
package com.crazymakercircle.designmodel.singleton;
//饿汉式
public class FSingleton {
// 类初始化时,会立即加载该对象,线程安全,调用效率高
private static final FSingleton instance = new FSingleton();
// 私有化构造方法
private FSingleton() {
}
public static FSingleton getInstance() {
return instance;
}
}
饿汉模式就是类一旦加载,就把单例初始化完成,保证getInstance的时候,单例是已经存在的了。
特点:
- 是否 Lazy 初始化:否
- 是否多线程安全:是
- 实现难度:易
优点:
- 没有加锁,执行效率会提高。
- 这种方式比较常用,但容易产生垃圾对象
- 它基于JVM class loader 机制, 是单线程执行的, 避免了多线程的同步问题
缺点:
- 类加载时就初始化,浪费内存,
懒汉模式
public class Singleton {
private volatile static Singleton instance; //①
private Singleton() { //②
}
public static Singleton getInstance() {
if (instance == null) {//③
synchronized (Singleton.class) {
if (instance == null) {//④
instance = new Singleton();//⑤
}
}
}
return instance;
}
}
而懒汉比较懒,只有当调用getInstance的时候,才回去初始化这个单例。
特点:
- 是否 Lazy 初始化:是
- 是否多线程安全:是
- 实现难度:难
1、线程安全:
饿汉式天生就是线程安全的,可以直接用于多线程而不会出现问题,
懒汉式本身是非线程安全的,需要通过多种手段,保证线程安全和内存可见性:
- volatile 保证内存可见性
- synchronized + 双重检查 保证线程安全
2、资源加载和性能:
饿汉式在类创建的同时就实例化一个静态对象出来,不管之后会不会使用这个单例,都会占据一定的内存,但是相应的,在第一次调用时速度也会更快,因为其资源已经初始化完成。
而懒汉式顾名思义,会延迟加载,在第一次使用该单例的时候才会实例化对象出来,第一次调用时要做初始化,如果要做的工作比较多,性能上会有些延迟,之后就和饿汉式一样了。
- 意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
- 主要解决:一个全局使用的类频繁地创建与销毁。
- 何时使用:当您想控制实例数目,节省系统资源的时候。
- 如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
- 关键代码:构造函数是私有的。
- 应用实例:
1、一个党只能有一个主席。
2、Windows是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。
3、一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。
优点:
1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
2、避免对资源的多重占用(比如写文件操作)。
缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
使用场景:
1、要求生产唯一序列号。
2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。
注意事项:getInstance() 方法中需要使用 Double Check 双重检查锁,synchronized (Singleton.class) 防止多线程同时进入造成instance 被多次实例化。
注:本文以 PDF 持续更新,最新尼恩 架构笔记、面试题 的PDF文件,请从下面的链接获取:语雀 或者 码云
推荐阅读:
-
《尼恩Java面试宝典》
-
《Springcloud gateway 底层原理、核心实战 (史上最全)》
-
《Flux、Mono、Reactor 实战(史上最全)》
-
《sentinel (史上最全)》
-
《Nacos (史上最全)》
-
《分库分表 Sharding-JDBC 底层原理、核心实战(史上最全)》
-
《clickhouse 超底层原理 + 高可用实操 (史上最全)》
-
《redis 集群 实操 (史上最全、5w字长文)》
-
《一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之间混乱关系(史上最全)》
-
《红黑树( 图解 + 秒懂 + 史上最全)》
-
《分布式事务 (秒懂)》
-
《缓存之王:Caffeine 源码、架构、原理(史上最全,10W字 超级长文)》
-
《缓存之王:Caffeine 的使用(史上最全)》
-
《Java Agent 探针、字节码增强 ByteBuddy(史上最全)》
-
《Docker原理(图解+秒懂+史上最全)》
-
《Redis分布式锁(图解 - 秒懂 - 史上最全)》
-
《Zookeeper 分布式锁 - 图解 - 秒懂》
-
《Zookeeper Curator 事件监听 - 10分钟看懂》
-
《Netty 粘包 拆包 | 史上最全解读》
-
《Netty 100万级高并发服务器配置》
-
《Springcloud 高并发 配置 (一文全懂)》