单例模式的各种实现方式(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单例模式几种实现方式
在平时的工作.学员的学习以及面试过程中,单例模式作为一种常用的设计模式,会经常被面试官问到,甚至笔试会要求学员现场默写,下面将会就单例模式的实现思路和几种常见的实现方式进行简单的分享. 单例模式,是一 ...
随机推荐
- cmake之引入外部项目(引用其他项目)、FetchContent管理子模块(fetchcontent用法)
本文CMAKE版本为3.18 演示环境: Windows+CMake+VS2017 源码下载说明 演示代码是后来传上去的,而且做了些修改,将spdlog_demo由exe改为了lib,但是,spdlo ...
- 【九度OJ】题目1144:Freckles 解题报告
[九度OJ]题目1144:Freckles 解题报告 标签(空格分隔): 九度OJ 原题地址:http://ac.jobdu.com/problem.php?pid=1144 题目描述: In an ...
- Problem 2233 ~APTX4869
Problem 2233 ~APTX4869 Accept: 55 Submit: 176Time Limit: 1000 mSec Memory Limit : 32768 KB Pro ...
- github项目托管方式(看项目自身是否自带有 .git)
一.本地仓库和远程仓库建立联系 方式一:项目自身带有 .git文件的[自身就是一个本地仓库的](这里咱以vue-cli3项目为例) 1.创建自带.git本地仓库:创建一个叫 my-vue 的项目 2. ...
- Chapter 7 Confounding
目录 7.1 The structure of confounding Confounding and exchangeability Confounding and the backdoor cri ...
- OpenCV 可自动调整参数的透视变换
在shiter大牛的基础之上,对于他的程序做了一定的修改. 首先,通过两个循环使得霍夫变换两个参数:角度的分辨率和点个数的阈值可以变换,这样就不必对于每一张图像都手动的设置阈值.其次,过滤掉了两个距离 ...
- 【MySQL作业】MySQL函数——美和易思字符串函数应用习题
点击打开所使用到的数据库>>> 1.将所有客户的姓名与电话以"-"作为分隔符进行连接显示. 使用 concat(s1,s2,-) 函数将所有客户的姓名与电话以&q ...
- MySQL数据库安装Version5.5
1.新建mysql用户 useradd -g hadoop -s /bin/bash -md /home/mysql mysql 创建.bash_profile,加载.bashrc 2.检查并且卸载系 ...
- 使用tomcat搭建HTTP文件下载服务器
使用tomcat搭建HTTP文件下载服务器, 有时我们的应用或者服务需要去外网下载一些资源, 但是如果在内网环境或者网络不好的情况下, 我们可以在内网提供文件下载服务, 将预先下载好的资源放在某个地方 ...
- 2048 双人创新小游戏【JavaFX-FXGL游戏框架】
一个 uml 课程的大作业,项目要求设计并开发一款 2048 与某种游戏类型相结合的创新游戏.可以选择只建模或者既建模又实现,既然要做当然是选择实现啦(虽然没有接触过游戏...期末周的莽冲hhh,小组 ...