Java多线程案例之单例模式懒汉+饿汉+枚举
作者:??未见花闻???? 发布时间:2021-11-07 05:18:01
前言:
本篇文章将介绍Java多线程中的几个典型案例之单例模式,所谓单例模式,就是一个类只有一个实例对象,本文将着重介绍在多线程的背景下,单例模式的简单实现。
1.单例模式概述
单例模式,是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中,应用该模式的类一个类只有一个实例,即一个类只有一个对象实例。
单例模式有两种典型的实现,一是饿汉模式,二是懒汉模式,饿汉模式中的“饿”并不是真的表示“饿”,更加准确的来说应该是表示“急”,那就是一个单例类还没被使用,它的单例对象就已经创建好了,而懒汉模式,要等到使用这个单例类时才创建单例对象。
单例模式中的单例类,只能拥有一个实例对象,又static
修饰的成员是属于类的,也就是只有一份,所以我们可以使用static
修饰的成员变量保存实例对象的引用。
2.单例模式的简单实现
2.1饿汉模式
由于单例模式中,一个类只能拥有一个实例对象,所以需要将类构造方法封装,防止类被创建多个实例对象,但是在使用该类时必须要得到该类的实例对象,因此我们得创建一个获取该唯一实例对象的方法getInstance
。
而对于该类的实例对象,在类中我们可以使用属于类的成员变量来保存(即static
成员变量)。
//单例模式 - 饿汉模式
class HungrySingleton {
//1.使用一个变量来保存该类唯一的实例,因为单例模式在一个程序中只能拥有一个实例,由于static成员只有一份,我们可以使用static变量来保存
private static final HungrySingleton instance = new HungrySingleton();
//2.封装构造方法,防止该类被实例出新的对象
private HungrySingleton() {}
//3.获取该类的唯一实例对象
public HungrySingleton getInstance() {
return instance;
}
}
多线程情况下,对于上述简单实现的饿汉式单例模式,只需要考虑getInstance
方法是否线程安全即可,由于该方法就一句返回语句,即一次读操作,而读操作是线程安全的,所以getInstance
方法也就是线程安全的,综上饿汉式单例模式是线程安全的。
2.2懒汉模式
懒汉模式相比于饿汉模式,区别就是实例对象创建时机不同,懒汉模式需要等到第一次使用时才创建实例对象,所以仅仅只需要修改获取对象的方法即可。
不考虑多线程情况,懒汉模式实现代码如下:
//单例模式 - 懒汉模式
class SlackerSingleton {
//1.使用一个变量来保存该类唯一的实例,因为单例模式在一个程序中只能拥有一个实例,由于static成员只有一份,我们可以使用static变量来保存
//懒汉单例模式是在使用的时候创建对象,因此初始时对象不应该被创建
private static SlackerSingleton instance;
//2.封装构造方法,防止该类被实例出新的对象
private SlackerSingleton() {}
//3.获取该类的唯一对象,如果没有就创建
public SlackerSingleton getInstance() {
if (instance == null) {
instance = new SlackerSingleton();
}
return instance;
}
}
多线程情况下,由于getInstance
方法中存在两次读(一次判断一次返回)操作一次写操作(修改intsance
变量的值),instance
变量为初始化时(即instance=null
)可能会存在多个线程进入判断语句,这样该类可能会被实例出多个对象,所以上述实现的懒汉式单例模式是线程不安全的。
造成线程不安全的代码段为if语句里面的读操作和instance
的修改操作,所以我们需要对这段代码进行加锁,然后就得到了线程安全的懒汉模式:
//多线程情况下饿汉模式获取对象时只读不修改,所以是线程安全的
//多线程情况下懒汉模式获取对象时存在两次读操作,分别为判断instance是否为null和返回instance,除了读操作还存在修改操作,即新建对象并使instance指向该对象
//懒汉模式对象还未初始化的时候,可能会存在多个线程进入判断语句,会导致实例出多个对象,因此懒汉单例模式是线程不安全的。
//线程安全单例模式 - 懒汉模式
class SafeSlackerSingleton {
//1.使用一个变量来保存该类唯一的实例,因为单例模式在一个程序中只能拥有一个实例,由于static成员只有一份,我们可以使用static变量来保存
//懒汉单例模式是在使用的时候创建对象,因此初始时对象不应该被创建
private static SafeSlackerSingleton instance;
//2.封装构造方法,防止该类被实例出新的对象
private SafeSlackerSingleton() {}
//3.获取该类的唯一对象,如果没有就创建
public SafeSlackerSingleton getInstance() {
synchronized (SafeSlackerSingleton.class) {
if (instance == null) {
instance = new SafeSlackerSingleton();
}
}
return instance;
}
}
但是!上述线程安全问题只出现在instance
没有初始化的时候,如果instance
已经初始化了,那个判断语句就是个摆设,就和饿汉模式一样,就是线程安全的了,如果按照上面的代码处理线程安全问题,不论instance
是否已经初始化,都要进行加锁,因此会使锁竞争加剧,消耗没有必要消耗的资源,所以在加锁前需要先判断一下instance
是否已经初始化,如果为初始化就进行加锁。
按照上述方案得到以下关于获取对象的方法代码:
public SafeSlackerSingletonPlus getInstance() {
//判断instance是否初始化,如果已经初始化了,那么该方法只有两个读操作,本身就是线程安全的,不需要加锁了,这样能减少锁竞争,提高效率
if (instance == null) {
synchronized (SafeSlackerSingletonPlus.class) {
if (instance == null) {
instance = new SafeSlackerSingletonPlus();
}
}
}
return instance;
}
到这里线程安全的问题是解决了,但是别忘了编译器它是不信任你的,它会对你写的代码进行优化! 上面所写的代码需要判断instance==null
,而多线程情况下,很可能频繁进行判断,这时候线程不会去读内存中的数据,而会直接去寄存器读数据,这时候instance
值变化时,线程完全感知不到!造成内存可见性问题,为了解决该问题需要使用关键字volatile
修饰instance
变量,防止编译器优化,从而保证内存可见性。
//线程安全优化单例模式 - 懒汉模式
class SafeSlackerSingletonPlus {
//1.使用一个变量来保存该类唯一的实例,因为单例模式在一个程序中只能拥有一个实例,由于static成员只有一份,我们可以使用static变量来保存
//懒汉单例模式是在使用的时候创建对象,因此初始时对象不应该被创建
private static volatile SafeSlackerSingletonPlus instance;
//2.封装构造方法,防止该类被实例出新的对象
private SafeSlackerSingletonPlus() {}
//3.获取该类的唯一对象,如果没有就创建
public SafeSlackerSingletonPlus getInstance() {
//判断instance是否初始化,如果已经初始化了,那么该方法只有两个读操作,本身就是线程安全的,不需要加锁了,这样能减少锁竞争,提高效率
//如果线程很多,频繁进行外层或内层if判断,可能会引发内层可见性问题,因此要给instan变量加上volatile
if (instance == null) {
synchronized (SafeSlackerSingletonPlus.class) {
if (instance == null) {
instance = new SafeSlackerSingletonPlus();
}
}
}
return instance;
}
}
2.3枚举实现单例模式
除了使用饿汉和懒汉模式还可以使用枚举的方式实现,在《Effective Java》书中有这样一句话:单元素的枚举类型已经成为实现Singleton的最佳方法。 因为枚举就是一个天然的单例,并且枚举类型通过反射都无法获取封装的私有变量,非常安全。
//单元素的枚举类型已经成为实现Singleton的最佳方法
enum EnumSingleton {
INSTANCE;
public void doSomething() {
System.out.println("完成一些任务!");
}
}
使用方式:
public class Singleton {
public static void main(String[] args) {
EnumSingleton.INSTANCE.doSomething();
}
}
运行结果:
来源:https://juejin.cn/post/7104061773992591373


猜你喜欢
- 1.设置url-pattern为*.do(最为常见的方式)只要你的请求url中包含配置的url-pattern,该url就可以到达Dispa
- 先看下面图片:这是我在做登录页面的时候,调用系统的ProgressDialog 进行等待,可是看起来很不协调,左边的等待图片过大,右边文字过
- C# 输出参数out什么是输出参数方法声明时,使用out修饰符声明的形参,成为输出参数。输出参数的特点1、输出参数不创建新的储存位置。2、输
- 一、Maven聚合开发_继承关系 Maven中
- 一、序言Java多线程编程线程池被广泛使用,甚至成为了标配。线程池本质是池化技术的应用,和连接池类似,创建连接与关闭连接属于耗时操作,创建线
- 首先我们在项目中导入这个框架:implementation 'com.mcxiaoke.volley:library:1.0.19&
- 本文实例讲述了C#多线程与跨线程访问界面控件的方法。分享给大家供大家参考。具体分析如下:在编写WinForm访问WebService时,常会
- 配置Servlet的方法有俩种,分别是传统web.xml文档中部署servlet和注解方式部署servlet,下面就先一起来学习 * 解方式部
- 本文实例为大家分享了Android实现弹幕效果的具体代码,供大家参考,具体内容如下首先分析一下,他是由三层布局来共同完成的,第一层视频布局,
- forword跳转页面的三种方式:1.使用serlvet/** * 使用forward跳转,传递基本类型参数到页面  
- 问题线上程序出现了OOM,程序日志中的输出为Exception in thread "http-nio-8080-exec-102
- 什么是 terms set 查询?Terms set 查询根据匹配给定字段的精确术语的最少数量返回文档。terms set 查询与 term
- 本文以实例形式讲述了基于Java的图的广度优先遍历算法实现方法,具体方法如下:用邻接矩阵存储图方法:1.确定图的顶点个数和边的个数2.输入顶
- 前言默认删除文件的时候 File.Delete 是将文件永久删除,如果是一些文档,建议删除到回收站,这样用户可以自己还原 通过 SHFile
- 1.两种取值方式的差异mapper.xml映射文件<select id="selectEmployeeByCondition
- Android程序调用本机googlemap,传递起始和终点位置,生成路线图if (wodeweizhiPoint != null) { i
- 前言:现在一般的Android软件都是需要不断更新的,当你打开某个app的时候,如果有新的版本,它会提示你有新版本需要更新。该项目实现的就是
- 一、银行存取款1.前言毕竟谁不喜欢钱呢!(不是😅)我看谁不喜欢在知识的海洋中遨游😤!2.描述银行存取款的流程是人们非常熟悉的事情,用户可以在
- 简单来说抽象类通常用来作为一个类族的最顶端的父类,用最底层的类表示现实中的具体事物,用最顶层的类表示该类族所有事物的共性。用abstract
- MyBatis resultMap id标签的错误使用我们在编写VO对象,如果业务场景稍微复杂一点,就会用到集合属性。例如用户查看个人订单列