单例模式实现

979 阅读5分钟

按照实例创建时间单例模式可分为两类

  • 提前加载

    在应用开始时创建实例.在早期的Java版本中无锁的线程安全单例模式

    private static final Singleton instance = new Singleton()

    被认为提前加载,但在新版本的Java版本中类只有在使用的时候才被加载,而不是在应用开始时即加载,所以这种方式某些情况下也是 延迟加载

  • 延迟加载:在getInstance方法首次被调用时创建

是否线程安全

getInstance()方法内部,如果只是

if (instance == null) {
    instance = new Singleton();
}

是无法保证线程安全的。如果在实例化对象时耗费时间比较长,极有可能在对象实例化完成前其他线程检测到insnce为空,又进行实例化操作。

线程安全的实现方式——同步锁单例模式

synchronized(SingletonSync.class){
    if (instance == null) {
        instance = new SingletonSync();
    }
}

在检测单例实例是否为空的代码块外加上SingletonSync类的同步锁,可以保证在同一时刻只能有一个线程能进入此代码块,达到线程安全的目的。

但另一方面,可以看到同步锁包裹的代码块只有在第一次执行getInstance()方法时才需要同步——如果单例已经被创建,那么任何线程都能线程安全的获取该实例

这种情况同步就会带来很多线程的等待,所以如果能在单例对象未实例化的情况下加锁,在已实例化的情况下不加锁,将会极大的减少线程的阻塞。

拥有双重校验锁机制的同步锁单例模式

if (instance == null){
    synchronized(SingletonSync.class){
        if (instance == null){
            instance = new SingletonSync();
        }
    }
}

先判断是否已经实例化,未实例化再加锁。 可以看到,我们对isntance == null判断了两次。

  • 第一次判断,经过上面的介绍很明显,是为了知道是否需要进行同步。
  • 那么第二次呢?——是因为可能存在多个线程同时执行到了第一次判断,且判断为真,这个情况就相当于有多个线程都认为未进行实例化,要对其进行实例化。如果在这里不进行第二次判断,第一个进程实例化完成后释放锁,第二个进程获取锁后将直接再一次的进行实例化操作,无法保证线程安全。进行第二次检查就能保证在首先获取锁的进程释放锁之后其他等待此锁的进程获得锁之后能知道已经实例化完成了,进而直接释放锁。

代码

package pattern.singleton;

import org.junit.jupiter.api.Test;


public class MySingleton {

    /*拥有双重校验锁机制的同步锁单例模式*/

	/* 
    volatile 关键字可以防止指令重排序,在getInstance()方法中创建MySingleton对象并赋值给instance的操作并不是原子的,可以分解为三步:
    1. 申请一块内存
    2. 初始化这块内存为相应的对象
    3. 赋值给instance
    如果出现指令重排序,3发生在2前面,那么程序在一个时间段内就会引用一个未初始化完成的对象,就会出现问题
    */
    private static volatile MySingleton instance = null;

    private MySingleton(){
        long time = 10000000;
        while (time >0){
            time--;
        }

    }

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

    public static MySingleton getInstanceNotSecurity(){
        if (instance == null) {
            instance = new MySingleton();
        }
        return instance;
    }

    public void doSomething(String threadName){
        System.out.println("do something in " + threadName + " with MyInstance " + this);
        System.out.println("this is equals to the static instance : " + (this == instance));
    }


}

class MyThread extends Thread{

    private String name;

    public MyThread(String name){
        this.name = name;
    }

    @Override
    public void run(){
        System.out.println("run my thread " + this.name);
        MySingleton singleton = MySingleton.getInstanceNotSecurity();
        System.out.println(singleton + " create in " + this.name);
        singleton.doSomething(this.name);
        System.out.println("thread " + this.name + " end");
    }
}

class TestMySingleton{
    @Test
    public void test(){
        MyThread t1 = new MyThread("t1");
        MyThread t2 = new MyThread("t2");
        t1.start();
        t2.start();
    }
}

执行结果

  • 使用没有加锁的实现方式
run my thread t1
run my thread t2
pattern.singleton.MySingleton@72bc151a create in t1
do something in t1 with MyInstance pattern.singleton.MySingleton@72bc151a
this is equals to the static instance : true
thread t1 end
pattern.singleton.MySingleton@173f92b7 create in t2
do something in t2 with MyInstance pattern.singleton.MySingleton@173f92b7
this is equals to the static instance : true
thread t2 end


Process finished with exit code 0

可以看到,两个线程分别实例化了一个对象。并且每个线程在执行doSomething()时都是用的自己实例化的对象。线程t1在实例化完成之后又进一步完成了doSomething()的执行,而后线程t2才完成实例化,把t1实例化的结果覆盖掉,然后执行了doSomething(),所以两次判断调用doSomething()方法的实例和当前的类变量instance是否相等时都是true

这是其中一种执行情况,还有可能出现下面的情况

run my thread t1
run my thread t2
pattern.singleton.MySingleton@2243a222 create in t2
pattern.singleton.MySingleton@173f92b7 create in t1
do something in t2 with MyInstance pattern.singleton.MySingleton@2243a222
do something in t1 with MyInstance pattern.singleton.MySingleton@173f92b7
this is equals to the static instance : true
this is equals to the static instance : false
thread t2 end
thread t1 end


Process finished with exit code 0
  • 同步的情况
run my thread t1
run my thread t2
pattern.singleton.MySingleton@4c159d6 create in t1
pattern.singleton.MySingleton@4c159d6 create in t2
do something in t1 with MyInstance pattern.singleton.MySingleton@4c159d6
do something in t2 with MyInstance pattern.singleton.MySingleton@4c159d6
this is equals to the static instance : true
this is equals to the static instance : true
thread t1 end
thread t2 end

或者

run my thread t1
run my thread t2
pattern.singleton.MySingleton@4c159d6 create in t2
pattern.singleton.MySingleton@4c159d6 create in t1
do something in t2 with MyInstance pattern.singleton.MySingleton@4c159d6
do something in t1 with MyInstance pattern.singleton.MySingleton@4c159d6
this is equals to the static instance : true
this is equals to the static instance : true
thread t2 end
thread t1 end

无论哪种执行顺序都能保证实例是单一的

补充:静态内部类方式实现

public class StaticSingleton {

    // 构造方法私有
    private StaticSingleton(){
       
    }

    private static class Inner{
        private static StaticSingleton instance = new StaticSingleton();
    }


    // 以静态方法返回实例
    public static StaticSingleton getInstance(){
        return Inner.instance;
    }
}
  • 构造方法私有化,保证只能在类内部创建实例
  • 静态内部类的静态域,保证只在静态内部类初始化的阶段实例化
  • 外部类加载的时候并不会将内部类一起加载(内部的类和普通类的加载时机是一样的,new、访问静态方法、访问静态变量等情况下),当第一次调用getInstance()方法时才会出发静态内部类的加载过程,在类加载的初始化阶段完成静态域的赋值,所以也是懒初始化的,懒汉模式。

参考

《JAVA设计模式及实践》2.1单例模式