单例设计模式
前言
在有些系统中,为了节省内存资源、保证数据内容的一致性,对某些类要求只能创建一个实例,这就是所谓的单例模式。
一、单例模式
单类模式能够保证某个类在程序中只存在一个实例,而不会创建多个实例:比如我们JDBC的DataSource实例,Windows的回收站,多线程的线程池等等
单例模式有 3 个特点:
- 单例类只有一个实例对象;
- 该单例对象必须由单例类自行创建;
- 单例类对外提供一个访问该单例的全局访问点;
单例模式有很多种,我们重点介绍饿汉模式和懒汉模式
饿汉模式
class Singleton{
//直接创建一个实例
private static Singleton instance = new Singleton();
//私有的构造方法
private Singleton(){
}
//通过Singleton.getInstance获取该实例
public static Singleton getInstance() {
return instance;
}
}
我们这个属性是类属性,与实例无关,我们java的每个类,在编译完成后都会得到一个.class文件,JVM运行时就会加载这个.class文件,读取其二进制指令,并在内存中构造出对应的类对象(Singleton.class)
当我们想手动的去创建一个Singleton对象时,发现报错了,Singleton()方法是私有的。
我们可以发现,我们在设计单例模式的时候,把构造方法设置为私有的了,就是防止外面的代码去new 对象。
我们对外提供获取的实例的方法,设置为public static,该方法为类方法,通过类名即可调用。
为什么这种方法叫做饿汉模式?
因为我们在类加载阶段就将实例创建出来了,类加载是我们程序比较靠前的一个阶段,所以我们形象的把它称之为饿汉模式。
懒汉模式
class Singleton{
private static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
我们有了饿汉模式的基础,我们对懒汉模式更好理解了。
我们可以发现,我们懒汉模式并没有在类加载阶段就创建实例,因为它很懒
我们可以发现,懒汉模式,只有在我们第一次调用时才会去创建对象,如果不用就不会创建对象。
二、线程安全问题
我们刚才所介绍的两种单例模式,在单线程下是安全的,我们来分析一下在多线程条件下,是否安全?
我们可以发现,我们的饿汉模式只涉及到读操作,在我们前面学习的线程安全问题可以知道,多个线程读一个数据,是线程安全的。
我们可以看到我们的懒汉模式,既涉及到了读操作,有涉及到了写操作。
我们发现多线程下的懒汉模式,可能会触发多少new操作,与单例模式的理念相反,所以我们的懒汉单例模式是线程不安全的。
既然懒汉模式涉及到读写问题,那我们就加锁呗。
public static Singleton getInstance() {
if(instance == null) {
synchronized (Singleton.class) {
instance = new Singleton();
}
}
return instance;
}
这样加锁可以吗?
不可以,刚才的线程问题,实际上就是读比较写三个操作不是原子的,给写加锁,仍然会造成脏读问题。
public static Singleton getInstance() {
synchronized (Singleton.class) {
if(instance == null) {
instance = new Singleton();
}
}
return instance;
}
我们把锁加在外面,才能保证读,比较,写三个操作为原子的。
我们保证原子性之后,我们t2在想去读的时候,就会阻塞等待,知道t1修改保存之后,t2才能load,此时load的结果是t1修改之后的值了,也就不会new对象了。
虽然保证了单例,但是我们的代码还是有一些不足之处。
我们多线程来获取实例时,不管该实例是否被创建,都会加锁,而加锁的开销又是比较大的,我们这个真的需要每次获取实例都加锁吗?
public static Singleton getInstance() {
if(instance == null) {
synchronized (Singleton.class) {
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
此时我们在加锁之前加一条判断,判断实例是否被创建,如果创建了就不再加锁,如果没有创建在进行加锁操作。
但是即使我们加了这样的判断,就一定能保证不会在实例创建后加锁吗?
当我们有很多线程,都在获取实例,我们CPU可能会进行优化,只有第一次读的是主内存的,修改后,后面的线程都是读的未更新的工作内存的值,这样仍会多次加锁,造成不必要的开销。
class Singleton{
private volatile static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if(instance == null) {
synchronized (Singleton.class) {
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
我们为了安全起见,我们给实例属性加上volatile关键字修饰,彻底去避免内存可见性问题和指令重排序问题。
相关文章
- 【设计模式】单例模式最常见的几种实现方法以及各自的特点
- 实践GoF的设计模式:单例模式
- 设计模式
- Java设计模式-单例模式(Singleton)
- python 高级篇--单例设计模式-
- java设计模式之单例模式
- Java学习笔记——单例设计模式Singleton
- 我所理解的设计模式(C++实现)——备忘录模式(Memento Pattern)
- Java设计模式透析之 —— 适配器(Adapter)
- 设计模式之单例模式详解及代码示例
- [设计模式]简单工厂模式
- 单例设计模式
- Java 基础(单例 Singleton 设计模式)
- Java 设计模式
- 23种设计模式
- Java设计模式单例 饿汉式 懒汉式
- js--设计模式--单例模式
- 设计模式之Composite模式(笔记)
- Java设计模式(三)-修饰模式
- JavaBean在DAO设计模式简介
- 设计模式之九 单例模式
- 设计模式。单例,枚举(完美中的完美)
- JAVA设计模式——第 8 章 适配器模式【Adapter Pattern】(转)
- 设计模式C++学习笔记之十(Builder建造者模式)
- Java 设计模式 接口型模式 之 类型介绍 (一)
- PHP设计模式——简单工厂
- Java设计模式—单例模式
- Koa.js 设计模式-学习笔记
- 大战设计模式(第二季)【2】———— 从源码看单例模式
- 大话设计模式之建造者模式