惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

阮一峰的网络日志
阮一峰的网络日志
Google DeepMind News
Google DeepMind News
Engineering at Meta
Engineering at Meta
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
宝玉的分享
宝玉的分享
Latest news
Latest news
aimingoo的专栏
aimingoo的专栏
云风的 BLOG
云风的 BLOG
美团技术团队
V
Visual Studio Blog
F
Full Disclosure
腾讯CDC
H
Help Net Security
D
DataBreaches.Net
M
MIT News - Artificial intelligence
罗磊的独立博客
博客园 - 司徒正美
N
Netflix TechBlog - Medium
U
Unit 42
Vercel News
Vercel News
I
InfoQ
S
SegmentFault 最新的问题
B
Blog RSS Feed
博客园 - 三生石上(FineUI控件)
Microsoft Security Blog
Microsoft Security Blog
有赞技术团队
有赞技术团队
博客园_首页
The GitHub Blog
The GitHub Blog
T
Tailwind CSS Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
NISL@THU
NISL@THU
P
Privacy & Cybersecurity Law Blog
Recorded Future
Recorded Future
Y
Y Combinator Blog
S
Schneier on Security
P
Proofpoint News Feed
T
Tenable Blog
Cloudbric
Cloudbric
博客园 - 【当耐特】
The Register - Security
The Register - Security
人人都是产品经理
人人都是产品经理
Google DeepMind News
Google DeepMind News
C
Cyber Attacks, Cyber Crime and Cyber Security
量子位
A
Arctic Wolf
N
News and Events Feed by Topic
H
Hacker News: Front Page
MongoDB | Blog
MongoDB | Blog
The Hacker News
The Hacker News
Webroot Blog
Webroot Blog

Java技术经验分享

生产故障处理SOP分享 | Java技术经验分享 系统稳定性建设实践总结【转载】 | Java技术经验分享 valine访问leancloud国际版异常,评论失效修复 | Java技术经验分享 匆匆忙忙的2021 | Java技术经验分享 聊一下换工作 | Java技术经验分享 IoT系列(2):WIFI设备常见配网方案介绍 | Java技术经验分享 IoT系列(1):什么是物联网 | Java技术经验分享 Java8特性2 - StreamApi | Java技术经验分享 Java8特性1 - lambda表达式&函数式接口 | Java技术经验分享 设计模式(5)-适配器模式 | Java技术经验分享 设计模式(4)-建造者模式 | Java技术经验分享 设计模式(3)-原型模式与浅拷贝和深拷贝 | Java技术经验分享 设计模式(2)-工厂模式图文介绍 | Java技术经验分享 Java时间处理5---Java8中时区相关类库介绍 | Java技术经验分享 Java时间处理4---Java8中LocalDate、LocalTime、LocalDateTime介绍 | Java技术经验分享 Java时间处理3---Java8中Instant、Duration、Period、Clock介绍 | Java技术经验分享 一些有意思的问答 | Java技术经验分享 Nacos系列博客说明 | Java技术经验分享 菜鸡程序员的2019年度总结 | Java技术经验分享 Java中“附近的人”实现方案讨论及代码实现 | Java技术经验分享 Java时间处理2----时区TimeZone类方法探究(Java8以前) | Java技术经验分享 Java时间处理1----Date和Calendar方法探究(Java8以前) | Java技术经验分享 FastJson中JSONString、JavaBean、JSONObject、JSONArray的转换关系及API示例 | Java技术经验分享 2019.11软考软件设计师归来心得体会及复习备考指南 | Java技术经验分享 你还没用过“约定式提交”吗?那你赶紧来补补知识吧 | Java技术经验分享 教你如何看懂UML中的类图及类图中的关系 | Java技术经验分享 设计模式总览 | Java技术经验分享 萌新入门Github请看这里,学不会远程教 | Java技术经验分享 Hexo的工作原理探究 | Java技术经验分享 Hexo-theme-butterfly修改调整记录教程 | Java技术经验分享 排序8:基数排序 | Java技术经验分享 排序7:归并排序 | Java技术经验分享 排序6:快速排序 | Java技术经验分享 排序5:冒泡排序 | Java技术经验分享 排序4:堆排序 | Java技术经验分享 排序3:选择排序 | Java技术经验分享 排序2:希尔排序 | Java技术经验分享 排序1:直接插入排序 | Java技术经验分享 推荐一款博客一文多发的良心工具OpenWrite | Java技术经验分享 近期学习计划 | Java技术经验分享 Nacos(九):Nacos集群部署和遇到的问题 | Java技术经验分享 Nacos(八):Nacos持久化 | Java技术经验分享 Nacos(七):Nacos共享配置 | Java技术经验分享 Nacos(六):多环境下如何“管理”及“隔离”配置和服务 | Java技术经验分享 Nacos(五):多环境下如何“读取”Nacos中相应的配置 | Java技术经验分享 Nacos(四):SpringCloud项目中接入Nacos作为配置中心 | Java技术经验分享 Nacos(三):Nacos与OpenFeign的对接使用 | Java技术经验分享 Nacos(二):SpringCloud项目中接入Nacos作为注册中心 | Java技术经验分享 Nacos(一):Nacos介绍 | Java技术经验分享 20190719小组分享 | Java技术经验分享 Java中equals和HashCode方法的分析 | Java技术经验分享 Java中==和equals方法的分析 | Java技术经验分享 Java中的自动拆装箱、装箱缓存 | Java技术经验分享 About-blog | Java技术经验分享 Java中的编译、反编译和反编译工具全家桶分享 | Java技术经验分享 finalize()的生命周期(执行过程) | Java技术经验分享 Java关键字之final、finally与finalize方法 | Java技术经验分享 Java中重写、重载 | Java技术经验分享 Java中面向对象的三大特征:继承、封装、多态 | Java技术经验分享 DockerFile介绍 | Java技术经验分享 Docker环境下安装Gitlab | Java技术经验分享 Docker中私有仓库的搭建流程 | Java技术经验分享 Centos7下两种方式安装Docker-CE | Java技术经验分享 Vert.x创建一个Http服务 | Java技术经验分享 Vert.x创建TCP服务端及客户端 | Java技术经验分享 Vert.x Core(二)- Event Bus(事件总线) | Java技术经验分享 Vert.x-Core(一)- 基础篇 | Java技术经验分享 SpringBoot项目中实现国际化 | Java技术经验分享 Vert.x介绍 | Java技术经验分享 毕设选题项目本地运行环境搭建教程 | Java技术经验分享 Jupyter Notebooks的安装和使用介绍 | Java技术经验分享 算法笔试题:1元,5元,10元,20元,50元、100元面值人民币组合给定x元的问题 | Java技术经验分享 Quartz学习总结 | Java技术经验分享 SpringBoot2.x集成Redis | Java技术经验分享 SpringBoot2.x集成MongoDB | Java技术经验分享 [SpringCloud学习] - 浅谈微服务架构 | Java技术经验分享 基于hexo和coding免费搭建个人博客网站 | Java技术经验分享 Hello World | Java技术经验分享
设计模式(1)-带你了解3类8种单例模式 | Java技术经验分享
文章作者: LarsCheng · 2020-08-29 · via Java技术经验分享

单例模式的分类

  • 饿汉式
    • 静态常量
    • 静态代码块
  • 懒汉式
    • 线程不安全
    • 线程安全,同步方法
    • 线程安全,同步代码块
    • 双重检查锁
    • 静态内部类
  • 枚举

饿汉式

饿汉式,单例模式的一种类型,对于这个名字可以假想成:

有一天小明买了菜回到家,由于他特别饿,于是就把所有菜都用掉做了满满一桌子菜,而直到最后吃饱,仍然有一些菜从来没尝过,而且由于做的菜太多导致的燃气也用完了。

这里的菜就是我们要使用的对象,而小明就是单例类,燃气就是系统内存。在调用方准备使用对象前,就把所有的对象都实例化好,以供随时调用,但如果实例化工作量过大可能导致内存浪费

饿汉式-静态常量(⭐慎用)

这是最简单的单例模式,主要有以下几点核心思路

  • 私有构造方法
  • 私有静态常量,类加载时初始化常量对象
  • 公有对象获取方法

示例代码如下

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
29
30
31
public class SingletonType01 {
public static void main(String[] args) {
Singleton01 instance1 = Singleton01.getInstance();
Singleton01 instance2 = Singleton01.getInstance();

System.out.println("instance1 == instance2 "+(instance1==instance2));
System.out.println(instance1.hashCode());
System.out.println(instance2.hashCode());
}
}

class Singleton01 {



private Singleton01() {
}



private final static Singleton01 INSTANCE = new Singleton01();






public static Singleton01 getInstance() {
return INSTANCE;
}
}

示例代码本机执行结果:

1
2
3
instance1 == instance2 true
491044090
491044090

主方法中对于两次获取到的对象进行了对比,可以看到两者为同一对象,且hashcode相同

优点:

  • 写法简单,在类装载的时候完成实例化,避免线程同步问题

缺点:

  • 在类装载时就实例化,那可能这个对象从始至终都没有被用到,无形中造成资源浪费,没有懒加载效果

这种单例模式,可以使用,并且无需考虑多线程问题,但是存在内存浪费问题

饿汉式-静态代码块(⭐慎用)

饿汉式静态代码块的实现与静态常量基本类似,唯一不同就是对象的实例化从静态变量转移到了静态代码块中,但其都是在类加载是执行的

代码如下

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42





public class SingletonType02 {
public static void main(String[] args) {
Singleton02 instance1 = Singleton02.getInstance();
Singleton02 instance2 = Singleton02.getInstance();

System.out.println("instance1 == instance2 : "+(instance1==instance2));
System.out.println(instance1.hashCode());
System.out.println(instance2.hashCode());
}
}

class Singleton02 {



private Singleton02() {
}



private static Singleton02 INSTANCE;




static {
INSTANCE = new Singleton02();
}





public static Singleton02 getInstance() {
return INSTANCE;
}
}

示例代码本机执行结果:

1
2
3
instance1 == instance2 : true
491044090
491044090

可以看出同样是单例对象的效果,所有与饿汉式静态常量写法相比较,其优缺点也一样,都会造成内存浪费

懒汉式

前面提到的两种单例模式都是饿汉式,即无论用不用这个对象,他对会被实例化。

这里要提到的是另一种单例模式-懒汉式,即对象只有在需要使用的时候才进行实例化,同样可以想象成一个小场景

有一天小李特别饿,但是他很懒,不想做饭就到餐馆吃饭,看了菜单从里面选择点了一份牛肉拉面,后厨师傅马上给他做好,小李吃饱后就开心的回家了

虽然描述的比较抽象,小李是是对象使用方,菜单上的每一个菜是一个单例类,后厨师傅是JVM。

当你选定一个对象了之后才会为你立即创建,而不是提前把所有的对象都实例化好。这样实现了懒加载的效果

懒汉式-线程不安全(👎👎👎不可使用)

懒汉式的简易版本,这一实现方式虽然做到了懒加载,但是存在线程安全问题

示例代码如下:

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
29
30
31
32
33
34
35
36
37
38

public class SingletonType03 {
public static void main(String[] args) {
Singleton03 instance1 = Singleton03.getInstance();
Singleton03 instance2 = Singleton03.getInstance();

System.out.println("instance1 == instance2 : " + (instance1 == instance2));
System.out.println(instance1.hashCode());
System.out.println(instance2.hashCode());
}
}

class Singleton03 {



private Singleton03() {
}




private static Singleton03 INSTANCE;








public static Singleton03 getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton03();
}
return INSTANCE;
}
}

示例代码本机执行结果:

1
2
3
instance1 == instance2 : true
491044090
491044090

简单的执行测试结果看似乎并无问题,做到了延迟加载(懒加载),并且实现了单例模式

但是!!!这一切都是单线程的前提下,一旦为多线程环境,在getInstance方法中会有严重的线程安全问题

分析:

  • 假设有两个线程A、B
  • A线程先到,判断INSTANCE为空,进入if内,准备进行对象初始化
  • 此时B线程也到达if判断,发现INSTANCE仍为空(A还未完成对象实例化),B也进入if内。

这种情况下,待A、B执行完后,得到的将是两个对象。这就完全违背了单例模式的初衷!!

所以通常情况下,不推荐使用这种懒汉式的单例模式。因为绝大多数的应用场景都为多线程环境。

而在多线程环境下,这种实现方式完全不算单例模式的范畴,因为它会产生多个对象实例

懒汉式 - 同步方法(👎不推荐)

针对于线程不安全问题,对应则有线程安全的解决方案

即在getInstance方法上加入synchronized关键字,将其改造成同步方法,解决在多线程环境下的线程不安全问题

示例代码:

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
class Singleton04 {
/**
* 构造方法私有,防止外部实例化
*/
private Singleton04() {
}

/**
* 静态私有变量
*/
private static Singleton04 INSTANCE;

/**
* 对外提供获取对象的静态方法,加入同步关键字,解决线程同步问题
* 外部调用,类名.方法名 Singleton.getInstance()
*
* @return 返回单例对象
*/
public static synchronized Singleton04 getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton04();
}
return INSTANCE;
}
}

如上,虽然解决了线程不安全问题,但是随之而来的是效率问题

分析:

  • 每次调用getInstance方法都需要进行线程同步
  • 实际上造成多个对象被实例化的仅仅只是方法中代码片段

所以总的来说,虽然解决的线程安全问题,但是由于效率不加,且有优化方案,故此种方式也不建议使用

针对同步方法带来的效率问题,有改进方案,但有一种错误的改进方案这里有必要提一下

同步方法改造为同步代码块,尝试减少同步的代码,来提高效率,示例代码如下:

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
class Singleton04ErrorSolution {



private Singleton04ErrorSolution() {
}




private static Singleton04ErrorSolution INSTANCE;







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

如上代码的本意是将同步方法细化到同步代码块,来进行效率优化,但是这样的改动起到了相反的效果

分析:

  • 对实例化对象的代码片段进行同步,假设A、B两线程执行getInstance方法
  • A线程判断INSTANCE为空后进入if内,准备执行同步代码块,此时B线程也判断INSTANCE为空,也进入了if内部,等待A线程执行完毕
  • A线程执行完同步代码块后,实例化了一个对象,此时B线程开始执行,也创建了一个对象

从上面的分析可以看出,这种改进方案,属于想法正确,但是操作错误,导致不但没有解决效率问题,同时造成线程安全问题,是一定要避免的错误!!

懒汉式-同步代码块(👎不推荐)

基于上文提到的优化思路:将同步方法细化到同步代码块,那正确的改进方案可能会有下面这种写法:

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

class Singleton05 {



private Singleton05() {
}




private static Singleton05 INSTANCE;







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

从getInstance方法可以看到,使用了同步代码块的方式,并且同步的是if判断和实例化部分的代码

虽然达到了线程安全,但是基本上和同步方法的效率没什么区别,依旧每个线程进来后,都需要等待执行同步代码块。

这种方案只是为了和上面的错误同步代码块方式进行对比。真实业务中也不推荐使用这种方式!!!

双重检查锁(👍推荐使用)

想要实现懒加载,同时保证线程安全,同时提高效率。那么一起来看看双重检查锁的实现方式:

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
29
30
class Singleton06 {



private Singleton06() {
}





private static volatile Singleton06 INSTANCE;







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

首先先来看看该方案于前几种的不同点

  • 使用synchronized关键字实现同步代码块
  • 同步前同步后两次判断
  • 使用了volatile关键字

分析

在getInstance方法中使用了Double-Check概念,配合同步代码块,保证线程安全。简单分析下其流程

  • A、B、C 3个线程执行getInstance方法
  • A、B线程都通过了第一个if判断,A线程抢到了锁,开始执行同步代码块中的逻辑,B等待
  • A通过了第二个if判断,进行了INSTANCE的实例化操作,A完成操作,释放锁
  • B开始执行同步代码块内容,B未通过第二个if(此时的INSTANCE不为空),直接返回INSTANCE对象,B释放锁
  • 此时C开始执行getInstance方法,C未通过第一个if,直接返回INSTANCE对象

从上面分析过程中可以看到,无论有多少个线程,实例化代码只会被执行一次,意味着只会创建一个对象。

volatile

但是在整个流程中有一个小小的隐患

INSTANCE = new Singleton06();它并非是一个原子操作,事实上,在 JVM 中上述语句至少做了以下这 3 件事:

①第一步:给 INSTANCE 分配内存空间;
②第二步:调用 Singleton06 的构造函数等,来初始化 INSTANCE;
③第三步:将 Singleton06 对象指向分配的内存空间(执行完这步 INSTANCE 就不是 null 了)。

这里的理想执行顺序是 1->2->3,实际在Jvm中执行顺序有可能是1->3->2,也有可能是 1->2->3。
这种现象被称作指令重排也就是说第 2 步和第 3 步的顺序是不能保证的,这就导致了隐患的产生。

在线程A执行INSTANCE = new Singleton06();是,JVM中的执行顺序是1->3->2,先进行分配内存再初始化INSTANCE,若在刚完成内存分配时,线程C开始执行第一个if判断,发现INSTANCE不为空,直接返回INSTANCE对象,此时的INSTANCE明显会出现问题。

在Java内存模型中,volatile 关键字作用可以是保证可见性且禁止指令重排。从而避免由于指令重排导致的异常隐患。

关于 volatile关键字和指令重排相关 可以参考此处

总结

双重检测锁的单例实现方案,可以实现延迟加载,同时线程安全并且效率高,在实际场景中是推荐使用的!

静态内部类(👍推荐使用)

除了双重检查锁被推荐使用外,静态内部类实现单例模式也是被推荐使用的一种
示例代码如下:

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

class Singleton07 {



private Singleton07() {
}




private static class SingletonInstance {
private static final Singleton07 INSTANCE = new Singleton07();
}







public static Singleton07 getInstance() {
return SingletonInstance.INSTANCE;
}
}

分析

  • 该方案采用了类装载机制来保证初始化实例时只有一个线程,从而保证了线程安全
  • 单例类Singleton07被装载时,静态内部类SingletonInstance是不会实例化的,只有调用getInstance方法时才会触发静态内部类SingletonInstance的装载,从而执行实例化代码
  • 并且静态内部类的静态属性只会在第一次加载类的时候被初始化,所以做到了懒加载

结论

保证了线程安全,使用静态内部类的特点实现懒加载,并且有较高效率,推荐使用

枚举(👍推荐使用)

那么这么多的实现方案,Java中有没有一个公认的最佳枚举实现方案呢,当然有啊,通过枚举来实现
代码如下:

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
29
30
31
32
33
public class SingletonType08 {
public static void main(String[] args) {
String connection1 = Singleton08.INSTANCE.getConnection();
String connection2 = Singleton08.INSTANCE.getConnection();

System.out.println("connection1 == connection2 : " + (connection1 == connection2));
System.out.println(connection2.hashCode());
System.out.println(connection2.hashCode());
}
}

enum Singleton08 {

INSTANCE;


private String connection = null;




Singleton08() {

this.connection = "127.0.0.1";
}




public String getConnection() {
return connection;
}
}

如上代码是通过枚举来实现单例对象的创建

enum有且仅有private的构造器,防止外部的额外构造,这恰好和单例模式吻合,也为保证单例性做了一个铺垫。
枚举类型是线程安全的,并且只会装载一次。

枚举类实现单例模式是 effective java 作者极力推荐的单例实现模式,它保证线程安全,并防止外部反序列化的破坏。