单例模式的三种写法

163 阅读4分钟

引言

还记得以前在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源码来深刻体会单例的优势。