单例模式的各种实现方式(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】1413. 逐步求和得到正数的最小值 Minimum Value to Get Positive Step by Step Sum
作者: 负雪明烛 id: fuxuemingzhu 个人博客:http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 求和 日期 题目地址:https://leetcode ...
- 【九度OJ】题目1109:连通图 解题报告
[九度OJ]题目1109:连通图 解题报告 标签(空格分隔): 九度OJ 原题地址:http://ac.jobdu.com/problem.php?pid=1109 题目描述: 给定一个无向图和其中的 ...
- Arm64架构下静态编译Nginx
这段时间,我一直忙于将 Rainbond 源码构建模块移植到 Arm64/aarch64 架构中.这一源码构建模块可以将指定代码仓库中包含的源码,拉取构建成为容器镜像,在各种容器平台中运行.目前支持的 ...
- RabbitMQ学习笔记三:Java实现RabbitMQ之与Spring集成
搭建好maven项目环境,加入RabbitMQ依赖包 <dependency> <groupId>org.springframework.amqp</groupId> ...
- Mac下搭建基于PlatformIO的嵌入式开发环境(STM32开发)
PlatformIO简介 PlatformIO是开源的物联网开发生态系统.提供跨平台的代码构建器.集成开发环境(IDE),兼容 Arduino,ESP8266和mbed等 支持在Windows.Lin ...
- 详解Kalman Filter
中心思想 现有: 已知上一刻状态,预测下一刻状态的方法,能得到一个"预测值".(当然这个估计值是有误差的) 某种测量方法,可以测量出系统状态的"测量值".(当然 ...
- Java EE数据持久化框架 • 【第2章 MyBatis实现DML操作】
全部章节 >>>> 本章目录 2.1 标签 2.1.1 标签简单应用 2.1.2 使用JDBC方式返回主键自增的值 2.1.3 使用标签返回普通主键的值 2.1.4 实践练 ...
- Java集合与数组的联系和区别
数组特点 存放一组相同的数据类型(基本类型和对象类型)的数据,从而实现对数据的管理. 优势:可以快速的通过下标对数组元素进行访问,效率高 劣势:容量实现定义好了,不能随着需求变化而扩容. 集合特点 集 ...
- RSA非对称加密算法实现:Python
RSA是1977年由罗纳德·李维斯特(Ron Rivest).阿迪·萨莫尔(Adi Shamir)和伦纳德·阿德曼(Leonard Adleman)一起提出的.当时他们三人都在麻省理工学院工作.RSA ...
- python requests发起请求,报“Max retries exceeded with url”
需要高频率重复调用一个接口,偶尔会出现"Max retries exceeded with url" 在使用requests多次访问同一个ip时,尤其是在高频率访问下,http连接 ...