单例模式的各种实现方式(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单例模式几种实现方式
在平时的工作.学员的学习以及面试过程中,单例模式作为一种常用的设计模式,会经常被面试官问到,甚至笔试会要求学员现场默写,下面将会就单例模式的实现思路和几种常见的实现方式进行简单的分享. 单例模式,是一 ...
随机推荐
- 【LeetCode】799. Champagne Tower 解题报告(Python)
作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 动态规划 参考资料 日期 题目地址:https:// ...
- 1254 - Prison Break
1254 - Prison Break PDF (English) Statistics Forum Time Limit: 2 second(s) Memory Limit: 32 MB Mic ...
- 自我学习与理解:keras框架下的深度学习(三)回归问题
本文主要是使用keras对其有的波士顿房价数据集做一个回归预测,其代码架构与之前一样(都只是使用多层感知机):数据的预处理.搭建网络框架.编译.循环训练以及测试训练的网络模型.其中除了数据预处理与之前 ...
- [源码解析] PyTorch 分布式之弹性训练(5)---Rendezvous 引擎
[源码解析] PyTorch 分布式之弹性训练(5)---Rendezvous 引擎 目录 [源码解析] PyTorch 分布式之弹性训练(5)---Rendezvous 引擎 0x00 摘要 0x0 ...
- Log4自定义Appender介绍
最初想要在执行一段业务逻辑的时候调用一个外部接口记录审计信息,一直找不到一个比较优雅的方式,经过讨论觉得log4j自定义的appender或许可以实现此功能.后来就了解了一下log4j的这部分. Ap ...
- 求最大公因数和最小公倍数(C++实现)
求两个正整数之最大公因子的算法(辗转相除法) 最大公约数是指能同时整除它们的最大正整数 基本原理:两个数的最大公约数等于它们中较小的数和两数之差的最大公约数. 就如有 a = 122, b = 54 ...
- web服务之nginx部署
本期内容概要 了解web服务 Nginx和Apache的对比 部署Nginx 内容详细 1.什么是web服务 Web服务是一种服务导向架构的技术,通过标准的Web协议提供服务,目的是保证不同平台的应用 ...
- 编写Java程序,使用Set实现不重复添加用户
返回本章节 返回作业目录 需求说明: 在控制台输入用户信息,用户信息包括姓名.性别和年龄,将用户信息保存至User对象中. 将User对象保存至HashSet集合中. 规定如果两个User对象的姓名. ...
- Java 获取客户端浏览器中的语言设置
获取客户端的首选语言 javax.servlet.ServletRequest.getLocale() 根据Accept-Language请求头返回客户端的首选语言.如果客户端请求没有Accept-L ...
- 初识python: 类练习 - 随机数生成
1.提取指定值之间的指定个数的随机整数. 2.继承生成随机数的类,打印"坐标". 生成随机数类: import random class GetRadndom(object): ' ...