单例模式 (饿汉懒汉)
Posted 一朵花花
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了单例模式 (饿汉懒汉)相关的知识,希望对你有一定的参考价值。
定义
单例模式,是一种常见的"设计模式"
设计模式: 设计模式是一套经过反复使用的代码设计经验,目的是为了重用代码、让代码更容易被他人理解、保证代码可靠性
单例模式,场景: 代码中,有些概念,不应该存在多个实例,此时应该使用单例模式来解决
例: mysql JDBC中,第一步就是创建一个 DataSourse 对象,DataSourse 对象,在一个程序中只有一个实例,不应该实例化多份DataSourse 对象
可以用单例模式来解决这种场景,保证指定的类只有一个实例 (若尝试创建多个实例,直接编译就会报错)
单例模式的实现
饿汉模式
类加载的同时,创建实例
(只要类被加载,就会立刻实例化 Singleton 实例)
代码:
public class ThreadDemo22
/*
* 饿汉模式 单例实现
* "饿" —— 只要类被加载,实例就会立刻被创建 (实例创建的时机比较早)
* */
static class Singleton
// 把构造方法变成私有的,此时在该类的外部就无法 new 这个类的实例了
private Singleton()
// 再来创建一个 static 的成员,表示 Singleton 类唯一的实例
// static 成员 和类相关,和实例是无关的
// 类在内存中只有一份,static 成员也只有一份
private static Singleton instance = new Singleton();
public static Singleton getInstance()
return instance;
public static void main(String[] args)
// 此处 new 可以,是因为 Singleton 是 ThreadDemo22 的内部类,
// ThreadDemo 是可以访问 内部类的 private 成员的
Singleton s = new Singleton();
// 此处的 getInstance 就是获取该类实例的唯一方式,不应该使用其他方式来创建实例
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2);
输出结果:
懒汉模式
当类被加载的时候,不会立刻实例化
等到第一次使用这个实例的时候,再实例化
public class ThreadDemo23
/*
* 懒汉模式
* */
static class Singleton
private Singleton()
// 类加载的时候,没有立刻实例化
// 第一次调用 getInstance 时,才真正的实例化
private static Singleton instance = null;
public static Singleton getInstance()
if(instance == null)
instance = new Singleton();
return instance;
类加载的时候,没有立刻实例化;第一次调用 getInstance 时,才真正的实例化
若代码,一直没有调用 getInstance,此时实例化的过程也就被省略掉了 —— 延时加载
一般认为,“懒汉模式” 比 “饿汉模式” 的效率更高~
原因: 懒汉模式有很大的可能是 “实例是用不到”,此时就节省了实例化的开销
线程安全问题分析:
思考: “饿汉模式” 和 “懒汉模式”,哪个是线程安全的???
(线程安全:假设多个线程并发的调用 getInstance 方法,是否会导致逻辑错误)
啥样的情况会导致线程不安全???
在线程安全问题,我们提到有以下原因:
- 线程是抢占式执行的
- 修改操作不是原子的
- 多个线程尝试修改同一个变量(单例模式常出现)
- 内存可见性
- 指令重排序
饿汉模式—线程安全;
懒汉模式—线程不安全
分析:
1.饿汉模式:
实例化时机是在类加载的时候,而类加载只有一次机会,不可能并发执行
当多线程并发的调用 getInstance 时,由于 getInstance 里只做了一件事:读取 instance 实例的地址,相当于多个线程在同时读取同一个变量;因此,饿汉模式是线程安全的
2.懒汉模式:
多线程同时调用 getInstance 时,getInstance 中做了四件事:①读取 instance 的内容;②判断是否为null;③若 instance 为 null,就 new 实例;④返回实例的地址
当 new 实例的时候,就会修改 instance 的值
画图分析:
懒汉模式,后续调用 getInstance 都不会触发线程安全问题,只有在第一次实例化的时候,多线程并发调用 getInstance 时,会有线程不安全问题的风险
如何解决线程安全问题??
那么,如何改进 懒汉模式,让代码变成线程安全的???
方法1— 加锁 synchronized
- 改法1
这样写,读取判断操作,和new 修改操作 仍然不是原子的,故这样修改不可行!!
private static Singleton instance = null;
public static Singleton getInstance()
if(instance == null)
synchronized (Singleton.class)
instance = new Singleton();
return instance;
- 改法2
这么加是可以保证原子性的
private static Singleton instance = null;
public static Singleton getInstance()
synchronized (Singleton.class)
if(instance == null)
instance = new Singleton();
return instance;
上述改法,虽然解决了线程不安全的问题,但仍然会问题 — 效率问题,
画图分析:
- 改法3
private static Singleton instance = null;
synchronized public static Singleton getInstance()
if(instance == null)
instance = new Singleton();
return instance;
画图和改法2 差不多,只不过 return 操作是在释放锁内部来完成的
由于 return 只是在读,所以这个操作放到锁里边或者锁外边不影响结果
虽然改法2 和 改法3 都可行,但是改法2 的锁粒度更小,改法3 的锁粒度更大
锁的粒度: 锁中包含的代码越多,就认为锁粒度越大
一般,我们希望锁的粒度小一点更好,因为锁的粒度越大,说明这段代码的并发能力就越受限
方法2 — 双重 if
由于加锁是为了避免第一次创建实例时线程不安全,后面在进行加锁解锁操作都只会降低性能,所以外层再添加 if 判断,当发现其为空时才加锁,否则直接返回已经创建好的实例对象,减少了加锁解锁的次数,从而提高性能
private static Singleton instance = null;
public static Singleton getInstance()
if(instance == null)
synchronized (Singleton.class)
if(instance == null)
instance = new Singleton();
return instance;
画图分析:
- 实例化之前:
此处有多个读操作,可能会被编译器优化:只有第一次读,才从内存中读,后续的读就是从CPU中读取寄存器(上次读到的结果)
这样就可能导致线程1 修改之后,线程2 没有读到最新的值
- 实例化之后:
为了改进上述可能出现的编译器优化的问题,再添加 volatile
方法3 — volatile
private volatile static Singleton instance = null;
public static Singleton getInstance()
if(instance == null)
synchronized (Singleton.class)
if(instance == null)
instance = new Singleton();
return instance;
懒汉模式的最终优化结果:
static class Singleton
private Singleton()
//创建static成员变量,标识Singleton类的唯一实例,为避免内存可见性问题,添加volatile
private volatile static Singleton instance = null;
public static Singleton getInstance()
// 加锁是为了避免第一个创建实例时线程不安全,后面在进行加锁解锁操作都只会降低性能
if (instance == null)
//如果为空,说明实例还未存在(即第一次使用),则创建实例
//加锁,确保判断为空和 new对象两个操作 成为原子操作
synchronized (Singleton.class)
if (instance == null)
instance = new Singleton();
return instance;
关键点总结
1.加锁 — 保证线程安全
2.双重 if — 保证效率
3.volatile — 避免内存可见性引发的问题
以上三点缺一不可
以上是关于单例模式 (饿汉懒汉)的主要内容,如果未能解决你的问题,请参考以下文章