引言
还记得以前在B站看一些Java入门教学视频的时候,上课的老师说如果面试官问你单例模式,一定要写饿汉式,因为线程安全,如果写懒汉式肯定要问你怎么解决线程安全问题。当初还觉得非常有道理,现在回想起来,***,真坑!本文就来介绍一下目前市面上最流行的三种单例写法。
单例模式介绍
单例模式是一种创建型模式,顾名思义,使用这种模式只会产生一个实例。具体来说,就是类提供了某个对象的创建接口,这个接口内部的一些细节能够保证每次都返回相同的实例。下面来看下具体如何实现这一模式。
饿汉式
饿汉式实现非常简单,因为它巧妙的利用了类加载机制实现了线程安全,即在初始化类变量阶段创捷了那个唯一实例,而在方法层面直接返回已创建的实例即可。但这样做的坏处是浪费内存,很可能这个实例一直得不到使用,但内存却实实在在被占用了。
/**
* 单例模式:饿汉式
*/
public class EagerSingleton {
public static SingleLinkedList instance = new SingleLinkedList();
private EagerSingleton() {}
/**
* 获取实例
*/
public static SingleLinkedList getInstance() {
return instance;
}
}
懒汉式
懒汉式的实现有三种方式:线程安全以及双重校验式
-
线程安全 线程安全版本的写法特点是: 1. 没有在类加载过程中直接 new 出实例 2. 在获取实例的接口上添加了方法锁,并在方法体内部进行实例创建判断,保证不重复创建实例 相比饿汉式,这种写法可以节省内存,但同样因为方法锁的存在会影响性能
/** * 单例模式:懒汉式 */ public class LazySingleton { public static SingleLinkedList instance; private LazySingleton() {} /** * 获取实例 */ public synchronized static SingleLinkedList getInstance() { if (instance != null) { instance = new SingleLinkedList(); } return instance; } } -
双重校验锁 毫不夸张地说,DCL是面试必考的知识点,可能不一定要你现场手撕,但是只要问到单例模式,一定会让你说出其实现细节,我们先来看看代码实现:
/** * 单例模式:双重校验锁(DCL,double checked locking) */ public class DoubleCheckedLockingTest { public volatile static DoubleCheckedLockingTest instance; // 通过这种私有化构造器的方式可以防止外部获取接口 private DoubleCheckedLockingTest(){} /** * 获取实例 */ public static DoubleCheckedLockingTest getInstance() { // 若已创建实例则直接返回 if (instance == null) { // 加锁保证创建实例过程的线程安全 synchronized (DoubleCheckedLockingTest.class) { // 若不进行判空,拿到锁后直接创建实例可能会存在以下情况 // 多个线程同时通过了第一次检查,但只要有一个线程成功创建,其他线程再创建就违背了单例的原则 if (instance == null) { instance = new DoubleCheckedLockingTest(); } } } return instance; } }首先,DCL的instance初始化(
public volatile static DoubleCheckedLockingTest instance;)就和其他写法不一样,细心的同学会发现这里多了一个 volatile 关键字,加这个关键字的原因和指令重排序有关。补充知识点:对象创建过程主要分为三步:1.分配内存空间;2.初始化对象;3.将对象指向刚分配的内存空间
在对象创建过程中,分配完内存空间后,就会进行初始化对象,然后将对象指向刚分配的内存空间。这两部操作调换顺序一般情况下是不影响对象创建的,但如果因为指令重排序而调换了执行顺序,那么在多线程环境下,可能会造成某个线程访问到一个未完全初始化的对象。
其次,DCL没有采用懒汉式的方法锁,而是通过锁代码块的形式进行细粒度控。
最后,解释一下为什么构造器前面要是有 private 权限,如果我们使用public,那么每次 new 一个类的实例都会创建一个新的instance,这样就起不到单例的效果,因此必须私有化构造器
public Main { public static void main(String[] args) { // 如果不私有化构造器,new 两个实例我们就能获取到两个不一样的instance DoubleCheckedLockingTest o1 = new DoubleCheckedLockingTest(); DoubleCheckedLockingTest o2 = new DoubleCheckedLockingTest(); } }
结语
看到这里,相比同学们对单例模式已经有了初步的了解,事实上Spring框架大量使用了这一设计模式,在后续的学习中,可以尝试阅读Spring源码来深刻体会单例的优势。