张三也能学得会的八种单例模式写法

1,405 阅读8分钟

单例模式

今天来和大家一起盘一下单例模式的几种写法,因为单例模式涉及到JVM、多线程相关的知识,因此也是面试时候的高频问题,那么我们一起来看一下吧

单例模式有懒汉式和饿汉式。懒汉式体现了懒加载的思想,只有到使用的时候才会进行创建,这样可以避免不必要的资源浪费。饿汉式则是在程序开始的时候就进行创建,这样做的好处是在程序运行时就做好准备,但缺点也很明显,如果程序中始终没有使用,那么会浪费响应的空间。

懒汉式

1.线程不安全的懒汉式(多线程环境下不推荐使用)

public class Singleton{  
  
private static  Singleton instance ;  
  
private Singleton(){}  
  
public static SingleTon getInstance(){  
    if(instance == null){  
        instance = new Singleton();  
    }  
    return instance;  
 }  

我们来分析一下这段懒汉式代码,当一个线程A需要获取单例对象时,他会先进入getInstance()方法体内,并判断if条件成立,进入条件代码块,而此时有另外一条线程B也想来获取单例对象,但此时线程A还没有执行new语句,也就是B线程也认为此时需要进行创建,那么两条线程分别创建了一个单例对象,也就是造成了线程不安全

2.粗粒度Synchronized懒汉式(多线程环境下不推荐使用)

public class Singleton{  
  
private static  Singleton instance ;  
  
private Singleton(){}  
  
public static Singleton synchronized getInstance(){  
	if(instance == null){  
		instance = new Singleton();  
    }  
	return instance;  
}  

我们来分析一下这段代码,与前面那段代码相比,我们在类方法上加上了Synchronized关键字,也就是使用该方法的时候会锁定当前Singleton.Class(对Synchronized关键字的朋友们可以看这里),而Class对象全局唯一,保证了一个时刻只有一个线程执行方法,也就是保证了线程安全,但是这种方法会造成线程阻塞等待锁,因此效率较低,并不推荐使用

3.细粒度Synchronized懒汉式(多线程环境下不推荐使用)

public class Singleton {
    private static Singleton singleton;

    private Singleton() {}

    public static Singleton getInstance() {
        if (singleton == null) {
            //注释一
            synchronized (Singleton.class) {
                singleton = new Singleton();
            }
        }
        return singleton;
    }
}

在这段代码中,我们通过在getInstance()方法中对Singleton.class进行加锁,那么这种写法是线程安全的嘛?答案是这种写法不是线程安全的,我们来一起分析一下,假设线程A和线程B都想要获取单例对象,那么他们会先调用getInstance()方法,然后进行if判断,假设这之前没有其他线程获取过单例对象,那么线程A和线程B都执行到注释一那里,然后假设线程B获取到了锁,线程A阻塞等待,直到线程B执行完代码块中的内容通过new创建了一个单例对象,然后A进入线程,重新创建单例对象,所以会造成线程不安全的情况。

4.静态内部类(推荐使用)

/**
 * 静态内部类实现懒汉单例
 */
public class LazySingletonByStaticInnerClass {
    private static class SingletonHolder{
        public static LazySingletonByStaticInnerClass instance = new LazySingletonByStaticInnerClass();
    }
    private LazySingletonByStaticInnerClass(){
    }
    public static LazySingletonByStaticInnerClass getInstance(){
        return SingletonHolder.instance;
    }
}

静态内部类的优点是:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存。即当LazySingletonByStaticInnerClass第一次被加载时,并不需要去加载SingletonHolder,只有当getInstance()方法第一次被调用时,才会去初始化INSTANCE,第一次调用getInstance()方法会导致虚拟机加载SingletonHoler类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。调用的是SingletonHolder.INSTANCE,取的是SingletonHoler里的INSTANCE对象,getInstance()方法并没有多次去new对象,故不管多少个线程去调用getInstance()方法,取的都是同一个INSTANCE对象,而不用去重新创建。当getInstance()方法被调用时,SingletonHolder才在SingleTon的运行时常量池里,把符号引用替换为直接引用,这时静态对象INSTANCE也真正被创建,然后再被getInstance()方法返回出去。

5.DCL懒汉单例(推荐使用)

/**
 * 懒汉式单例 在用到单例对象时才会初始化,线程安全
 */
public class LazySingletonByDoubleLockCheck {
    /**
     * volatile关键字防止指令重拍
     */
    private volatile static LazySingletonByDoubleLockCheck instance = null;
    private LazySingletonByDoubleLockCheck(){
    }
    public static LazySingletonByDoubleLockCheck getInstance(){
        /**
         * 双检锁保证线程安全
         */
        if(instance==null){
            //注释一
            synchronized (LazySingletonByDoubleLockCheck.class){
                //注释二
                if (instance ==null){
                    LazySingletonByDoubleLockCheck lazySingleton = new LazySingletonByDoubleLockCheck();
                }
            }
        }
        return instance;
    }
}

DCL(double check lock)懒汉式,也叫做双检锁懒汉式,他是通过在getInstance()方法中通过两次if语句判断当前单例对象是否已经存在,从而保证了线程安全。

回想我们之前的那个细粒度Synchronized懒汉式,他是在同步代码块之前进行了一次if判断,这样就会造成多线程环境下一条线程进入同步代码块创建单例对象,其他线程阻塞在同步代码块前等待获取锁,当持有锁的线程离开,其他线程进入代码块的时候会在注释二处再进行一次判断所需要的单例是否已经创建并存在,这样就保证了后续线程不会重新创建单例对象。

DCL单例模式中还有一个重要的点,就是需要在静态字段上用volatile关键字进行修饰,volatile关键字的作用有两个,一个是保诚线程间变量的可见性,一个是防止指令重排序,关于volatile关键字今天先不细讲,等这周末我会写一篇比较详细的文章进行介绍。这里volatile关键字的两个作用都会体现出来,假设我们有线程A和线程B,线程A先获取锁,进入同步代码块,并通过new LazySingletonByDoubleLockCheck();创建单例对象,这里我们需要注意的是,通过new创建对象并不是一个原子性的操作,在Java虚拟机中会分为三步进行,1,为创建对象分配内存,2.初始化对象,3.将对象指向分配好的内存地址,这三个步骤并不一定是顺序执行的,在java虚拟机中可能会根据实际情况进行指令重拍例如按照1,3,2的顺序进行执行,假设当前按照1,3,2的顺序执行,当步骤3执行完之后线程A退出同步代码块,但是线程A在对象创建的步骤2还未完成,也就是并没有初始化对象,此时单例对象在内存中还为null,并且此时线程B进入同步代码块,认为单例对象并未进行创建,随即执行单例的创建工作,这样也会造成线程不安全的情况。但是我们加上volatile关键字后,会禁止指令重排序,也就是创建对象的过程严格按照1,2,3的顺序进行,且instance的值对所有线程都可见,这样就保证了线程B进入同步代码块的时候能够知道单例对象已经创建,从而直接退出同步代码块,保证了线程安全。

饿汉式

1.通过静态常量创建饿汉式单例(推荐使用)

/**
 * 饿汉单例,变量在声明时就被初始化
 */
public class HungrySingleton {
    private static HungrySingleton instance = new HungrySingleton();
    private HungrySingleton(){

    }
    public static HungrySingleton getInstance(){
        return instance;
    }
}

在上述代码中,这个类含有一个静态字段instance,Java虚拟机会在准备阶段为静态字段分配空间,并为其赋为零值(由于是引用类型,这里的零值也就是null,如果是int基本类型,则是真正的数字0),然后会在初始化阶段才调用后面的new语句创建对象,因此在后续程序运行的时候,这个单例也已经创建好了,因此成为饿汉式单例

2.通过静态代码块创建饿汉式单例(推荐使用)

public class StaticSingleton {

    private static StaticSingleton instance;

    static {
        instance = new StaticSingleton();
    }

    private StaticSingleton() {}

    public static StaticSingleton getInstance() {
        return instance;
    }
}

在静态代码块这种方式中,由于静态代码块会先编译成字节码文件,静态代码块会由Javac编译器生成类构造器(),()方法会在初始化阶段执行,因此这种单例模式也是在程序运行之前就创建好

饿汉式的单例模式都是线程安全的,而下面的懒汉式单例模式,由于在使用的时候才会创建,因此有些懒汉式在多线程环境下无法保证线程安全

3.枚举单例(推荐使用)

public enum SingletonEnum {
    instance;
}

枚举单例是一种线程安全且高效的单例写法,在获取单例对象的时候只需要执行SingletonEnnum.instance就可以获取,十分简单

欢迎大家关注我的微信公众号:码外狂徒