单例模式-一问到底
Posted 双斜杠少年
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了单例模式-一问到底相关的知识,希望对你有一定的参考价值。
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式。在 GOF 书中给出的定义为:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
单例模式一般体现在类声明中,单例的类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
单例模式有以下两个优点:
-
在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如网站首页页面缓存)。
-
避免对资源的多重占用(比如写文件操作)。
有时候,我们在选择使用单例模式的时候,不仅仅考虑到其带来的优点,还有可能是有些场景就必须要单例。比如类似"一个党只能有一个主席"的情况。
实现
- 懒汉,
- 饿汉,
- 双重校验锁,
- 枚举
- cas
1. 懒汉模式
所谓"懒汉",指的是并不会事先初始化出单例对象,而是在第一次使用的时候再进行初始化,懒汉模式有两种写法,分别是线程安全的和非线程安全的。
懒汉模式可以在第一次真正用到的时候再实例化,避免了创建无效的对象。但是缺点是第一次使用的时候需要耗费时间进行对象的初始化。
//线程不安全的懒汉模式:
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;
private Singleton ()
public static synchronized Singleton getInstance()
if (instance == null)
instance = new Singleton();
return instance;
2. 饿汉模式
就是使用static定义全局对象,在对象声明的时候直接new一个对象,或者使用静态代码块。
所谓"饿汉",是指等不及要赶紧创建单例对象,即在类加载的过程中就进行单例对象的创建。具体实现方式也有多种。
//静态成员变量 饿汉模式:
public class Singleton
private static Singleton instance = new Singleton();
private Singleton ()
public static Singleton getInstance()
return instance;
//静态代码块 饿汉模式变种:
public class Singleton
private Singleton instance = null;
static
instance = new Singleton();
private Singleton ()
public static Singleton getInstance()
return this.instance;
//静态内部类
public class Singleton
private static class SingletonHolder
private static final Singleton INSTANCE = new Singleton();
private Singleton ()
public static final Singleton getInstance()
return SingletonHolder.INSTANCE;
2.1 饿汉如何保证线程安全
通过定义静态的成员变量,以保证单例对象可以在类初始化的过程中被实例化。和通过定义静态的内部类,以保证单例对象可以在类初始化的过程中被实例化。
这其实是利用了ClassLoader的线程安全机制。ClassLoader的loadClass方法在加载类的时候使用了synchronized关键字。
静态内部类,这种方式和静态代码块、成员变量只有细微差别,只是做法上稍微优雅一点。这种方式是Singleton
类被装载了,INSTANCE 不一定被初始化。因为SingletonHolder
类没有被主动使用,只有显示通过调用getInstance
方法时,才会显示装载SingletonHolder
类,从而实例化instance
。。。但是,原理和饿汉一样。
所以, 除非被重写,这个方法默认在整个装载过程中都是线程安全的。所以在类加载过程中对象的创建也是线程安全的。
3. 双重校验锁单例
通过同步代码块代替了懒汉模式中的同步方法,来减小锁的粒度,减少阻塞。但是避免并发,需要进行两次非空判断,所以叫做双重锁校验。
3.1双重锁校验的单例中为什么要使用volatile
因为编译器有可能进行指令重排优化,使得singleton对象再未完成初始化之前就对其进行了赋值,这样其他人拿到的对象就可能是个残缺的对象了。使用volatile的目的是避免指令重排。保证先进性初始化,然后进行赋值
//双重校验锁实现单例:
public class Singleton
private volatile static Singleton singleton;
private Singleton ()
public static Singleton getSingleton()
if (singleton == null)
synchronized (Singleton.class)
if (singleton == null)
singleton = new Singleton();
return singleton;
3.2 指令重排
java内存模型(jmm)并不限制处理器重排序,在执行**instance=new Singleton();**时,并不是原子语句,实际是包括了下面三大步骤:
- 1.为对象分配内存
- 2.初始化实例对象
- 3.把引用instance指向分配的内存空间
这个三个步骤并不能保证按序执行,处理器会进行指令重排序优化,存在这样的情况:
优化重排后执行顺序为:1,3,2, 这样在线程1执行到3时,instance已经不为null了,线程2此时判断instance!=null,则直接返回instance引用,但现在实例对象还没有初始化完毕,此时线程2使用instance可能会造成程序崩溃。
现在要解决的问题就是怎样限制处理器进行指令优化重排。
5.volatile double check 懒汉模式
在JDK1.5之后,使用volatile关键字修饰instance就可以实现正确的double check单例模式了
4. 枚举实现单例
public enum DataSourceEnum
DATASOURCE;
private DBConnection connection = null;
private DataSourceEnum()
connection = new DBConnection();
public DBConnection getConnection()
return connection;
枚举实现单例的好处
除了写法简单,几行代码就能搞定了,线程安全(枚举其实底层是依赖Enum类实现的,这个类的成员变量都是static类型的,并且在静态代码块中实例化的,和饿汉有点像, 所以他天然是线程安全的。)以外,枚举还有一个好处,那就是"枚举可以解决反序列化会破坏单例的问题"
在枚举序列化的时候,Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。
普通的Java类的反序列化过程中,会通过反射调用类的默认构造函数来初始化对象。所以,即使单例中构造函数是私有的,也会被反射给破坏掉。由于反序列化后的对象是重新new出来的,所以这就破坏了单例。
但是,枚举的反序列化并不是通过反射实现的。所以,也就不会发生由于反序列化导致的单例破坏问题。
5. CAS(AtomicReference)实现单例
不使用synchronized和lock,怎么实现一个线程安全的单例?
借助CAS(AtomicReference)实现单例模式:
public class Singleton
private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<Singleton>();
private Singleton()
public static Singleton getInstance()
for (;;)
Singleton singleton = INSTANCE.get();
if (null != singleton)
return singleton;
singleton = new Singleton();
if (INSTANCE.compareAndSet(null, singleton))
return singleton;
用CAS的好处在于不需要使用传统的锁机制来保证线程安全,CAS是一种基于忙等待的算法,依赖底层硬件的实现,相对于锁它没有线程切换和阻塞的额外消耗,可以支持较大的并行度。 CAS的一个重要缺点在于如果忙等待一直执行不成功(一直在死循环中),会对CPU造成较大的执行开销。
参考:
单例模式的七种写法:https://www.iteye.com/blog/cantellow-838473
他轻蔑的问我:你还说你了解单例模式吗:https://mp.weixin.qq.com/s/L12lHC0-ieEGqTDmIDkNtQ
以上是关于单例模式-一问到底的主要内容,如果未能解决你的问题,请参考以下文章
面试官猛的一问:Spring的Bean注入如何解决循环依赖的?