单例模式的各种实现方式(Java)
单例模式的基础实现方式
手写普通的单例模式要点有三个:
- 将构造函数私有化
- 利用静态变量来保存全局唯一的单例对象
- 使用静态方法
getInstance()
获取单例对象
懒汉模式
懒汉模式指的是单例对象的延迟加载,只有在调用 getInstance()
获取单例对象时才会将单例创建出来。懒汉模式适用于对内存要求高的场景。代码如下:
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
饿汉模式
与懒汉模式相对的是饿汉模式,适用于对内存要求不高的场景,在类加载的初始化阶段就完成了单例对象的创建,代码如下:
public class Singleton {
// 静态变量初始化
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
静态变量的初始化是在类加载阶段的初始化过程进行,在此期间,编译器会自动收集类中所有静态变量的赋值动作和 static
块,生成 <clinit>
方法并执行。比较特殊的一点是,如果多个线程同时初始化 Singleton
类,JVM 会保证只有一个线程能够执行 Singleton
类的 <clinit>
方法,其他线程都必须阻塞等待。而且同一个类加载器下,一个类只会被初始化一次,即 <clinit>
方法只会被执行一次,这就保证了多线程下单例对象只会被创建一次
版权:本文版权归作者和博客园共有
转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任
多线程下的单例模式
单例模式需要保证的一点是,在整个程序运行期间,单例对象只会被创建一次。如果是单线程环境中,这一点很好保证。但如果是多线程环境中,保证这一点并不简单
上面已经说过,饿汉模式的单例模式下,JVM 会保证单例对象只会被创建一次,因此可以保证这一点。而懒汉模式在多线程环境中不能保证这一点,接下来讨论的是对懒汉模式进行改造,让它能够保证这一点
使用synchronized方法
最简单直接的方式就是为 getInstance()
加上 synchronized
关键字,这样确实可以保证多线程环境中,单例对象只会被创建一次。但是 synchronized
方法最大的缺点在于它将获取单例对象这一行为彻底串行化,同一时刻只能有一个线程能执行 getInstance()
,大大降低了并发效率
代码如下:
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
双重检测锁
直接使用 synchronized
方法降低效率的主要原因在于,synchronized
方法的加锁粒度太粗,那么将锁的范围缩小,就可以缓解这一问题,而双重检测锁就是这么实现的。不过为了保证并发的正确性,在内部又加了一道检测,故名为双重检测锁。代码如下:
public class Singleton {
// 这里的instance一定要定义为volatile变量!!!
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
// 双重锁检测
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
上面代码的关键点有三个:
synchronized
加锁的范围更小,这是为了更高的并发效率synchronized
内部还有一道检测,如果线程1进入了同步块,但还未将单例对象创建出来,此时线程2正好绕过了第一道检测,在同步块外等待获取锁定。因此同步块内也要加上一道检测,避免单例对象被重复创建instance
这个变量一定要声明为volatile
!volatile
在这里最大的作用是禁止指令重排序。如果不加volatile
修饰,由于instance = new Singleton()
可能被重排序而导致在这条语句执行过程中,instance
率先被分配内存并获得地址,成为非 null,但构造函数却没有真正执行完毕,此时别的线程可能拿到的instance
就是不完全构造的单例对象
instance = new Singleton()
这条语句正常的执行顺序是:
1、为即将创建的对象分配一块内存
2、执行构造函数中的语句,对内存进行相应的读写操作
3、让 instance
指向这块内存
在重排序情况下顺序可能是 1 -> 3 -> 2,当执行到3时 instance
就成为非 null,此时其他线程如果引用了 instance
,拿到的就是一个不完全构造的对象
需要注意的是,在 JDK5 之前,就算加了
volatile
关键字也依然有问题,原因是之前的 JMM 是有缺陷,volatile
变量前后的代码仍然可以出现重排序问题,这个问题在 JDK5 之后才得到解决,所以现在才可以这么使用
版权:本文版权归作者和博客园共有
转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任
其他单例模式的实现方式
基于枚举类
基于枚举类的方式非常简洁,只要简单地编写一个只包含一个元素的枚举类,由 JVM 来保证单例的唯一性和线程安全性,自带私有的构造方法并且序列化和反射都不会破坏单例的唯一性,据说是 JDK5 之后最好的单例创建方式
public enum Singleton {
instance;
// 定义各种字段、方法
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
其中枚举类的构造器不用特意加上 private
修饰,因为枚举类构造器默认就是 private
的,且只能使用 private
修饰
简单理解枚举实现单例的过程:程序启动时,会自动调用
Singleton
的构造器,实例化单例对象并赋给instance
,之后再也不会实例化,这也是一个饿汉过程,即使没有调用过getInstance()
,也会将单例对象创建出来
使用枚举来创建单例模式的优势有3点:
- 代码量更少,更加简洁
- 没有做任何额外的操作,就可以保证单例的唯一性和线程安全性
- 使用枚举类可以防止调用者使用反射、序列化和反序列化机制强制生成多个单例对象,破坏唯一性
这第三点优势让基于枚举类的单例模式变得“无懈可击”了,枚举类可以保证唯一性的原理如下:
- 防反射
枚举类默认继承了 Enum
类,在利用反射调用 newInstance()
时,会判断该类是否是枚举类,如果是则抛出异常
- 防反序列化创建多个枚举对象
对于枚举类型,由于枚举类和枚举变量的组合名是唯一的,可以唯一确定对象。因此,序列化只会将枚举类名 + 枚举变量名输出到文件中。反序列化时,读入的就是枚举类名 + 枚举变量名,再根据 Enum
类的 valueOf
方法,在内存中找对已经存在的枚举对象,并不会创建新的对象
类加载器对单例模式的影响
同一个类加载器对一个类只会加载一次,但是不同的类加载器可能会多次加载同一个类,如果程序中有多个类加载器,需要在单例中指定某个特定的类加载器,并保证这个类加载器始终是同一个
单例模式的各种实现方式(Java)的更多相关文章
- 单例模式,多种实现方式JAVA
转载请注明出处:http://cantellow.iteye.com/blog/838473 第一种(懒汉,线程不安全): public class Singleton { private stati ...
- python实现单例模式的三种方式及相关知识解释
python实现单例模式的三种方式及相关知识解释 模块模式 装饰器模式 父类重写new继承 单例模式作为最常用的设计模式,在面试中很可能遇到要求手写.从最近的学习python的经验而言,singlet ...
- Atitit.dwr3 不能显示错误具体信息的解决方式,控件显示错误具体信息的解决方式 java .net php
Atitit.dwr3 不能显示错误具体信息的解决方式,控件显示错误具体信息的解决方式 java .net php 1. Keyword/subtitle 1 2. 使用dwr3的异常convert处 ...
- Redis实现分布式锁的正确使用方式(java版本)
Redis实现分布式锁的正确使用方式(java版本) 本文使用第三方开源组件Jedis实现Redis客户端,且只考虑Redis服务端单机部署的场景. 分布式锁一般有三种实现方式: 1. 数据库乐观锁: ...
- Android 源码解析:单例模式-通过容器实现单例模式-懒加载方式
本文分析了 Android 系统服务通过容器实现单例,确保系统服务的全局唯一. 开发过 Android 的用户肯定都用过这句代码,主要作用是把布局文件 XML 加载到系统中,转换为 Android 的 ...
- 总结java中文件拷贝剪切的5种方式-JAVA IO基础总结第五篇
本文是Java IO总结系列篇的第5篇,前篇的访问地址如下: 总结java中创建并写文件的5种方式-JAVA IO基础总结第一篇 总结java从文件中读取数据的6种方法-JAVA IO基础总结第二篇 ...
- Java实现单例模式的两种方式
单例模式在实际开发中有很多的用途,比如我们在项目中常用的工具类,数据库等资源的连接类.这样做的好处是避免创建多个对象,占用内存资源,自始自终在内存中只有一个对象为我们服务. 单例对象一般有两种实现方式 ...
- Java实现单例模式的几种方式
单例模式(Singleton),保证在程序运行期间,内存中只有一个实例对象. 饿汉式,最常用的方式.JVM加载类到内存中时,创建实例,线程安全. public class Boss { private ...
- Java单例模式几种实现方式
在平时的工作.学员的学习以及面试过程中,单例模式作为一种常用的设计模式,会经常被面试官问到,甚至笔试会要求学员现场默写,下面将会就单例模式的实现思路和几种常见的实现方式进行简单的分享. 单例模式,是一 ...
随机推荐
- Docker 与 K8S学习笔记(十)—— 容器的端口映射
我们一般将应用部署在容器里面,而一个服务器上会有许许多多的容器,那么外界该如何访问我们的应用呢?答案是:端口映射. Docker可以将容器对外提供服务的端口映射到host的某个端口上,外网通过此端口访 ...
- Causal Inference
目录 Standardization 非参数情况 Censoring 参数模型 Time-varying 静态 IP weighting 无参数 Censoring 参数模型 censoring 条件 ...
- Chapter 15 Outcome Regression and Propensity Scores
目录 15.1 Outcome regression 15.2 Propensity scores 15.3 Propensity stratification and standardization ...
- 使用PyTorch构建神经网络以及反向传播计算
使用PyTorch构建神经网络以及反向传播计算 前一段时间南京出现了疫情,大概原因是因为境外飞机清洁处理不恰当,导致清理人员感染.话说国外一天不消停,国内就得一直严防死守.沈阳出现了一例感染人员,我在 ...
- ROS机器人导航一 : 从英雄联盟到ROS导航
写在前面: 这是这个系列的第一篇 本系列主要从零开始深入探索ROS(机器人操作系统)的导航和规划. 这个系列的目标,是让大家了解: 1.ROS的导航是怎么实现的 2.认识ROS里各种已有的导航算法,清 ...
- .net core中Grpc使用报错:The response ended prematurely.
当我们调用Grpc是出现下面的一堆异常时,一般是由于LTS导致的: Call failed with gRPC error status. Status code: 'Unavailable', Me ...
- js 简单版发布留言 案例
<!DOCTYPE html> <html lang="en"> <head> <meta charset=" ...
- JMeter_事务控制器
性能测试的结果统计时我们一定会关注TPS,TPS代表的是每秒事务数,每个事务对应的是我们的请求.虽然JMeter能够帮我们把每个请求统计成一个事务,但有时候我们希望把多个操作统计成一个事务,JMete ...
- antd-vue中的form表单label标签for导致点击文字触发输入框解决方案
<a-form-item :label="label+'图片'" :label-col="{ span: 2 }" :wrapper-col=" ...
- .gitignore文件编写规则
1.gitignore说明 在使用git的过程中,一般我们总会有些文件无需纳入git的管理,也不希望它们总出现在未跟踪文件列表,这些文件通常是日志文件.临时文件.编译产生的中间文件.工具自动生成的文件 ...