单例模式的各种实现方式(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用法总结
欢迎指正 CMake : A.download : https://cmake.org/download/ B.tutorial: https://cmake.org/cmake-tutorial/ ...
- 【LeetCode】334. Increasing Triplet Subsequence 解题报告(Python)
[LeetCode]334. Increasing Triplet Subsequence 解题报告(Python) 标签(空格分隔): LeetCode 题目地址:https://leetcode. ...
- DP? (hdu3944)
DP? Time Limit: 10000/3000 MS (Java/Others) Memory Limit: 128000/128000 K (Java/Others)Total Subm ...
- fork之后,子进程从父进程那继承了什么(转载)
转载自:https://blog.csdn.net/xiaojun111111/article/details/51764389 知道子进程自父进程继承什么或未继承什么将有助于我们.下面这个名单会因为 ...
- Java面向对象笔记 • 【第4章 抽象类和接口】
全部章节 >>>> 本章目录 4.1 抽象类 4.1.1 抽象方法和抽象类 4.1.2 抽象类的作用 4.1.3 实践练习 4.2 final修饰符 4.2.1 final ...
- Android 悬浮窗
悬浮窗是一种比较常见的需求.例如把视频通话界面缩小成一个悬浮窗,然后用户可以在其他界面上处理事情. 本文给出一个简单的悬浮窗实现.可缩小activity和还原大小.可悬浮在其他activity上.使用 ...
- Linux查找class类所在jar包
1.说明 写代码或者定位问题的时候, 经常发生只知道类名不知道其所在jar包的问题, 在Eclipse中可以使用Ctrl+Shift+T查找类, 但是如果类所在的jar包不在Build Path中, ...
- Tcpdump抓包命令使用
tcpdump命令需要使用root执行 1. 查看网卡命令 ifconfig 2. 监视编址到指定端口的TCP或UDP数据包,那么执行以下命令: tcpdump -i eth0 host 10.43. ...
- 使用tomcat搭建HTTP文件下载服务器
使用tomcat搭建HTTP文件下载服务器, 有时我们的应用或者服务需要去外网下载一些资源, 但是如果在内网环境或者网络不好的情况下, 我们可以在内网提供文件下载服务, 将预先下载好的资源放在某个地方 ...
- MongoDB分片设计
#### 如何做好分片集群 * 合理的架构 * 是否需要分片? * 要分多少片? * 数据分布规则? * 正确的姿势 * 选择需要分片的表 * 选择正确的片键 * 使用合适的均衡策略 * 足够的资源 ...