设计模式之单例模式(Singleton Pattern)深入浅出
单例模式介绍:单例模式是指确保一个类在任何情况下都绝对只有一个实例,并且提供一个全局的访问点。隐藏其所有构造方法,属于创新型模式。
常见的单例有:ServletContext、ServletConfig、ApplicationContext、DBPool
单例模式的优点:
- 在内存中只有一个实例,减少内存开销。
- 可以避免对资源的占用
- 设置全局访问点,严格控制访问
单例模式的缺点:
- 没有接口,扩展困难
- 如果要扩展单例对象,只有修改代码,没有其他捷径
以下是单例模式的种类及优缺点分析
饿汉式单例
在单例类首次加载时就创建实例
第一种写法:
public class HungrySingleton {
private static final HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return hungrySingleton;
}
}
第二种写法:
public class HungryStaticSingleton {
private static final HungryStaticSingleton hungrySingleton;
static {
hungrySingleton = new HungryStaticSingleton();
}
private HungryStaticSingleton() {
}
public static HungryStaticSingleton getInstance() {
return hungrySingleton;
}
}
缺点:单例实例在类装载时就构建,浪费资源空间
懒汉式单例
一、懒汉式第一种:
首先先简单实现以下
public class LazySimpleSingleton {
private static LazySimpleSingleton lazySingleton = null;
private LazySimpleSingleton() {
}
public static LazySimpleSingleton getInstance() {
if (lazySingleton == null) {
lazySingleton = new LazySimpleSingleton();
}
return lazySingleton;
}
}
我们用线程测一下在多线程场景下会不会出现问题
先创建一个线程类
public class ExectorTread implements Runnable {
@Override
public void run() {
LazySimpleSingleton instance = LazySimpleSingleton.getInstance();
System.out.println(Thread.currentThread().getName() + ":" + instance);
}
}
测试
public class LazySimpleSingletonTest {
public static void main(String[] args) {
Thread t1 = new Thread(new ExectorTread());
Thread t2 = new Thread(new ExectorTread());
t1.start();
t2.start();
System.out.println("Exector End");
}
}
运行结果

结果发现创建的对象不一样
如果在方法上加入锁会解决问题
public class LazySimpleSingleton {
private static LazySimpleSingleton lazySingleton = null;
private LazySimpleSingleton() {
}
public synchronized static LazySimpleSingleton getInstance() {
if (lazySingleton == null) {
lazySingleton = new LazySimpleSingleton();
}
return lazySingleton;
}
}
虽然jdk1.6之后对synchronized性能优化了不少,但是还是存在一定的性能问题,这种写法会造成整个类被锁住,大大降低了性能
于是我们有了新的写法
二、懒汉式第二种:
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton lazySingleton = null;
private LazyDoubleCheckSingleton() {
}
// 适中方案
// 双重检查锁
public static LazyDoubleCheckSingleton getInstance() {
if (lazySingleton == null) {
synchronized (LazyDoubleCheckSingleton.class) {
if (lazySingleton == null) {
lazySingleton = new LazyDoubleCheckSingleton();
}
}
}
return lazySingleton;
}
}
知识补充:
1、线程安全性开发遵循三个原则:
- 原子性:即一个操作或者多个操作要么全部执行,要么都不执行
- 可见性:多个线程访问同一变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改的值
- 有序性:程序执行的顺序按照代码的先后顺序执行
通过对这段代码线程的debug发现,这里双重检查体现了可见性
2、JVM:CPU在执行的时候会转换成JVM指令
lazySingleton = new LazySimpleSingleton(); 这行代码实际进行了如下操作
- 第一步、分配内存给对象
- 第二步、初始化对象
- 第三步、将初始化对象和内存地址关联(赋值)
- 第四步、用户初次访问
在多线程环境中,第二步和第三步可能会发生颠倒,这就需要指令重排序,于是我们在变量声明上加上volatile关键字就很好的解决了问题
三、懒汉式第三种
通过内部类的方式实现
public class LazyInnerClassSingleton {
private LazyInnerClassSingleton() {
}
public static final LazyInnerClassSingleton getInstance() {
return LazyHolder.LAZY;
}
private static class LazyHolder {
private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
}
}
性能上最优的一种写法,全程没有用到synchronized,
通过懒加载饿汉式写法达到了懒汉式的目的,LazyHolder里面的逻辑要等到外面调用才执行,巧妙地运用了内部类的特性
有人会问这个不用考虑线程安全吗?其实这是利用了JVM底层的执行逻辑,完美的避开了线程安全性的问题
但是我们会考虑另一个问题,该类构造器虽然私有了,但是还是会被反射攻击,难逃反射法眼
我们来测试一下
public class LazyInnerClassSingletonTest {
public static void main(String[] args) {
try {
// 调用者装B,不走寻常路,显然搞坏了单例
Class<LazyInnerClassSingleton> clazz = LazyInnerClassSingleton.class;
Constructor<LazyInnerClassSingleton> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);//强吻(问)
LazyInnerClassSingleton instance = constructor.newInstance();
System.out.println(instance);
// 正常调用
LazyInnerClassSingleton instance2 = LazyInnerClassSingleton.getInstance();
System.out.println(instance2);
System.out.println(instance == instance2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行结果

针对反射问题我们有了以下解决办法
public class LazyInnerClassSingleton {
private LazyInnerClassSingleton() {
if (LazyHolder.LAZY != null){
throw new RuntimeException("不允许构建多个实例");
}
}
public static final LazyInnerClassSingleton getInstance() {
return LazyHolder.LAZY;
}
private static class LazyHolder {
private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
}
}
在私有构造方法上加上判断,如果已经对象被初始化就抛出异常
好的,被反射破坏的问题解决了,还会想到另一个问题,如果被反序列化对象还是单例吗?
知识点补充:反序列化是将已经持久的的字节码内容,转换为IO流,在转换过程中重新创建对象new。
我们拿饿汉式单例测试一下
单例类:
public class SeriableSingleton implements Serializable {
private static final SeriableSingleton singleton = new SeriableSingleton();
private SeriableSingleton() {
}
public static SeriableSingleton getInstance() {
return singleton;
}
}
测试类:
public class SeriableSingletonTest {
public static void main(String[] args) {
FileOutputStream fso = null;
SeriableSingleton s1 = null;
SeriableSingleton s2 = SeriableSingleton.getInstance();
try {
fso = new FileOutputStream("SeriableSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fso);
oos.writeObject(s2);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (SeriableSingleton) ois.readObject();
ois.close();
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行结果

显然反序列化破坏了单例
现在我们通过源码寻找答案
进入readObject()方法

方法通过调用readObject0(false)返回结果,再次进入readObject0()方法

找到object类型,调用了checkResolve(readOrdinaryObject(unshared)),进入readOrdinaryObject方法

在这里我们找到了实例化对象的语句
obj = desc.isInstantiable() ? desc.newInstance() : null;
意思是如果这个对象能被初始化就实例化对象,否则等于null
在这里打个断点调试一下,确实实例化了对象,存在私有构造方法也会实例化对象
接着往下看

如果desc.hasReadResolveMethod()返回true,就调用Object rep = desc.invokeReadResolve(obj);返回obj
进入hasReadResolveMethod


源码分析过后发现这个hasReadResolveMethod()方法是用来判断readResolve方法是否存在,如果存在返回true,不存在返回false
再看invokeReadResolve()方法

返回了readResolve这个方法的返回值,
所以经过这个判断会重新加载对象并返回
接下来我们得出结论,代码增加重写方法readResolve
public class SeriableSingleton implements Serializable {
private static final SeriableSingleton singleton = new SeriableSingleton();
private SeriableSingleton() {
}
public static SeriableSingleton getInstance() {
return singleton;
}
protected Object readResolve() {
return singleton;
}
}
再次运行解决了序列化的问题
但是值得我们注意的是,重写readResolve方法只不过是覆盖了反序列化出来的对象,对象还是创建了2次,
由于发生再JVM层面,相对来说比较安全,在之前没有被引用的对象会被GC回收(JVM知识点)
注册式单例
一、第一种写法
使用枚举类实现单例模式,也是《Effictive Java》这本书推荐的写法
public enum EnumSingleton {
INSTANCE;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumSingleton getInstance() {
return INSTANCE;
}
}
1、首先判断线程安全性
通过反编译工具JAD得到枚举类的源码,附:JAD下载地址
public final class EnumSingleton extends Enum
{ public static EnumSingleton[] values()
{
return (EnumSingleton[])$VALUES.clone();
} public static EnumSingleton valueOf(String name)
{
return (EnumSingleton)Enum.valueOf(com/zc/singleton/register/EnumSingleton, name);
} private EnumSingleton(String s, int i)
{
super(s, i);
} public Object getData()
{
return data;
} public void setData(Object data)
{
this.data = data;
} public static EnumSingleton getInstance()
{
return INSTANCE;
} public static final EnumSingleton INSTANCE;
private Object data;
private static final EnumSingleton $VALUES[]; static
{
INSTANCE = new EnumSingleton("INSTANCE", 0);
$VALUES = (new EnumSingleton[] {
INSTANCE
});
}
}
通过代码发现,静态代码块实例化了单例类,属于饿汉式单列,不存在线程安全性问题,这个实例化过程发生在JVM层面,所以可以认为懒加载
2、测试序列化
public class EnumSingletonTest {
public static void main(String[] args) {
FileOutputStream fso = null;
EnumSingleton s1 = null;
EnumSingleton s2 = EnumSingleton.getInstance();
s2.setData(new Object());
try {
fso = new FileOutputStream("EnumSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fso);
oos.writeObject(s2);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("EnumSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (EnumSingleton) ois.readObject();
ois.close();
System.out.println(s1.getData());
System.out.println(s2.getData());
System.out.println(s1.getData() == s2.getData());
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行结果 :

枚举类是怎样避免不被序列化破坏的呢?我们来查看源码
首先进入枚举类型case

进入readEnum方法

通过枚举类对象根据注册的类名获取实例然后返回,所以不会创建新的对象
3、测试反射
// 反射
try {
Class<EnumSingleton> clazz = EnumSingleton.class;
Constructor<EnumSingleton> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
constructor.newInstance(); }catch (Exception e){
e.printStackTrace();
}
运行结果 :

报错:没有找到这样的构造方法
反编译获取的类有这样的一个构造方法
private EnumSingleton(String s, int i)
{
super(s, i);
}
我们用这个构造再实例化一次看看
测试:
// 反射
try {
Class<EnumSingleton> clazz = EnumSingleton.class;
Constructor<EnumSingleton> constructor = clazz.getDeclaredConstructor(String.class,int.class);
constructor.setAccessible(true);
EnumSingleton instance = constructor.newInstance("zhou", 666);
System.out.println(instance); }catch (Exception e){
e.printStackTrace();
}
运行结果:

报错:不能通过反射创建这个枚举对象
查看源码:

得知如果该类的类型为枚举类,就抛出异常
总结:从JDK层面就为枚举类不被实例化和反射保驾护航
二、第二种写法
容器式单例,Spring容器中单例的写法
public class ContainerSingleton {
private ContainerSingleton() {}
private static Map<String, Object> ioc = new ConcurrentHashMap<String, Object>();
public static Object getBean(String className){
synchronized (ioc){
if (!ioc.containsKey(className)){
Object obj = null;
try {
obj = Class.forName(className).newInstance();
ioc.put(className,obj);
}catch (Exception e){
e.printStackTrace();
}
return obj;
}
return ioc.get(className);
}
}
}
优点:对象方便管理,其实也是属于懒加载
ThreadLocal
使用ThreadLocal实现单例模式
//伪线程安全
public class ThreadLocalSingleton { private ThreadLocalSingleton(){} private static final ThreadLocal<ThreadLocalSingleton> threadLocalSingleton = new ThreadLocal<ThreadLocalSingleton>(){
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}
}; public static ThreadLocalSingleton getInstance(){
return threadLocalSingleton.get();
}
}
测试多线程
public class ExectorTread implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
}
}
public class ThreadLocalSingletonTest {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName() + ":" +ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread().getName() + ":" +ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread().getName() + ":" +ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread().getName() + ":" +ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread().getName() + ":" +ThreadLocalSingleton.getInstance());
Thread t1 = new Thread(new ExectorTread());
t1.start();
Thread t2 = new Thread(new ExectorTread());
t2.start();
}
}
打印结果

结论:该单例在单个线程中可以保持单例,但是每个其他线程互相都不一样
原理:每次获取实例会从ThreadLocalMap中取值,而每个单例的key就是线程名
属于注册式单例(容器形式)
应用场景:Spring的orm框架中

以上对单例模式的介绍到此结束,欢迎批评指正。 附:源码地址
设计模式之单例模式(Singleton Pattern)深入浅出的更多相关文章
- 乐在其中设计模式(C#) - 单例模式(Singleton Pattern)
原文:乐在其中设计模式(C#) - 单例模式(Singleton Pattern) [索引页][源码下载] 乐在其中设计模式(C#) - 单例模式(Singleton Pattern) 作者:weba ...
- 设计模式之单例模式(Singleton Pattern)
单例模式 单例模式(Singleton Pattern)在java中算是最常用的设计模式之一,主要用于控制控制类实例的数量,防止外部实例化或者修改.单例模式在某些场景下可以提高系统运行效率.实现中的主 ...
- 【设计模式】单例模式 Singleton Pattern
通常我们在写程序的时候会碰到一个类只允许在整个系统中只存在一个实例(Instance) 的情况, 比如说我们想做一计数器,统计某些接口调用的次数,通常我们的数据库连接也是只期望有一个实例.Windo ...
- 二十四种设计模式:单例模式(Singleton Pattern)
单例模式(Singleton Pattern) 介绍保证一个类仅有一个实例,并提供一个访问它的全局访问点. 示例保证一个类仅有一个实例. Singleton using System; using S ...
- 乐在其中设计模式(C#) - 单例模式(Singleton Pattern)【转】
介绍 保证一个类仅有一个实例,并提供一个访问它的全局访问点. 示例 保证一个类仅有一个实例. Singleton using System; using System.Collections.Gene ...
- Java 设计模式(三)-单例模式(Singleton Pattern)
1 概念定义 1.1 定义 确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例. 1.2 类型 创建类模式 1.3 难点 1)多个虚拟机 当系统中的单例类被拷贝运行在多 ...
- python 设计模式之单例模式 Singleton Pattern
#引入 一个类被设计出来,就意味着它具有某种行为(方法),属性(成员变量).一般情况下,当我们想使用这个类时,会使用new 关键字,这时候jvm会帮我们构造一个该类的实例.这么做会比较耗费资源. 如果 ...
- 【UE4 设计模式】单例模式 Singleton Pattern
概述 描述 保证一个类只有一个实例 提供一个访问该实例的全局节点,可以视为一个全局变量 仅在首次请求单例对象时对其进行初始化. 套路 将默认构造函数设为私有, 防止其他对象使用单例类的 new运算符. ...
- 浅谈设计模式--单例模式(Singleton Pattern)
题外话:好久没写blog,做知识归纳整理了.本来设计模式就是个坑,各种文章也写烂了.不过,不是自己写的东西,缺少点知识的存在感.目前还没做到光看即能记住,得写.所以准备跳入设计模式这个大坑. 开篇先贡 ...
- 设计模式系列之单例模式(Singleton Pattern)——确保对象的唯一性
模式概述 模式定义 模式结构图 饿汉式单例与懒汉式单例 饿汉式单例 懒汉式单例 模式应用 模式在JDK中的应用 模式在开源项目中的应用 模式总结 主要优点 适用场景 说明:设计模式系列文章是读刘伟所著 ...
随机推荐
- PHP代码审计分段讲解(8)
20 十六进制与数字比较 源代码为: <?php error_reporting(0); function noother_says_correct($temp) { $flag = 'flag ...
- let和var变量的思考
刚学JavaScript,纠结全局变量用var 还是 let. 这篇文章[来源于知乎]表示 在定义全局变量时,var 和 let 的作用相同. 那么现在基本遵守ES6规范的前提下,函数变量还是for循 ...
- linux c++ 内存泄漏检测工具:AddressSanitizer(ASan)
1.介绍 AddressSanitizer(ASan),该工具为gcc自带,4.8以上版本均可以使用. 2.使用 编译的方式很简单,只需要添加 -fsanitize=address -g 即可,如 g ...
- 从HBase底层原理解析HBASE列族不能设计太多的原因?
在之前的文章<深入探讨HBASE>中,笔者详细介绍了: HBase基础知识(包括简介.表结构).系统架构.数据存储 WAL log和HBase中LSM树的应用 HBase寻址机制 mino ...
- Codeforces Edu Round 65 A-E
A. Telephone Number 跟之前有一道必胜策略是一样的,\(n - 10\)位之前的数存在\(8\)即可. #include <iostream> #include < ...
- 题解-[CEOI2017]Building Bridges
[CEOI2017]Building Bridges 有 \(n\) 个桥墩,高 \(h_i\) 重 \(w_i\).连接 \(i\) 和 \(j\) 消耗代价 \((h_i-h_j)^2\),用不到 ...
- 一、Nginx笔记--linux下载安装部署Nginx
Nginx 到底是什么? Nginx 是⼀个⾼性能的HTTP和反向代理web服务器,核⼼特点是占有内存少,并发能⼒强 Nginx ⼜能做什么事情(应⽤场景) Http服务器(Web服务器) 性能⾮常 ...
- React中对render进行的小优化
react中state和props变化会造成render的重新渲染,有时候我们会在render函数中进行一些稍微复杂的逻辑运算 比如说,像下边这种 在props中将 industries引入,然后对其 ...
- expdp、impdp状态查看及中断方法
一.expdp状态查看及中断方法 1.查询expdp的job的名字 SQL> select job_name from dba_datapump_jobs; JOB_NAME---------- ...
- Maven基础知识详解
1. 简介 Maven在Java领域的应用已经非常广泛了,有了Maven的存在是的开发人员在搭建.依赖.扩展和打包项目上变得非常简单. 2. Windows安装Maven 下载安装包 http ...