单例模式与线程安全

    单例模式,在设计模式中这是最为简单,也最容易理解的一种设计模式。它要求需要单例的类,在整个系统中,只能有唯一一个实例对象,这个对象被全局共享的。这个模式有哪些应用场景呢?比如说我们桌面的回收站,就是典型的单例模式,还有经常写web的小伙伴应该了解与数据库交互的DAO层也多是单例,spring默认注入的bean如果不特殊声明,默认也是单例实现,多线程的线程池设计一般也是采用单例模式。
    单例模式就是在全局中只能创建唯一实例,那么它就要必须能自行创建这个实例,而且可以自行想整个系统提供这个实例。这就要求单例模式的类只提供私有的构造的函数,类定义中含有一个该类的静态私有对象,该类需要提供一个静态的公有函数用于创建或者获取它本身的静态私有对象。

    题目是单例模式与线程安全,那如果只涉及单线程的场景,那就没什么好说了。这里主要讨论的是多线程场景下单例模式。而且,我们不再讨论饿汉式的单例模式,因为饿汉式的单例模式本身就是线程安全的,无论我们的系统是否需要,在一开始系统加载的时候就生成了这个实例,这是一种以空间换时间的方式,我们来看一下懒汉式的单例写法:

public class Singleton {
    private static Singleton singleton = null;
    private static volatile Integer num = 0;
    private Singleton() {}

    public static Singleton getSingleton() {
        if (singleton == null) {
            System.out.println(++num);
            singleton = new Singleton();
        }
        return singleton;
    }
}

下面是测试类:

public class TestSingleton {

   public static void main(String[] args) {
       for (int i = 0; i < 10000; i++) {
           new Thread(new Runnable() {
               @Override
               public void run() {
                   Singleton.getSingleton();
               }
           }).start();
       }
   }
}

    我们这里模拟了10000个线程同时去拿这个单例对象,如果说最终得到的始终是同一个对象,这里的结果,打印出来的应该只有一个1。我们来看下结果。
单例模式与线程安全_第1张图片
    显然结果拿到的出现了多个对象,说明这种单例的写法不是线程安全的。那我们用synchronized来修饰,看看能否得到线程安全的单例呢。

public class Singleton {
    private static volatile Singleton singleton = null;
    private static volatile Integer num = 0;
    private Singleton() {}

	public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized(Singleton.class) {
                if (singleton == null) {
                    System.out.println(++num);
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

    哈哈,这种测试了很多遍,结果只打印出了一个1,证实是线程安全的,为什么singleton还要用volatile修饰,这里就涉及到了java编译重排序的问题,大家可以看看《深入理解java虚拟机》这本书,讲的很详细。我这里只粗略的谈一下,singleton = new Singleton()这一行代码并不是一个原子操作,它主要干了三件事。哪三件呢,来看一看:
1.开辟一段内存
2.对象初始化赋值
3.将singleton指向开辟好的内存,完成对象的创建。
    如果正常按照1 2 3 的顺序来执行的话,一点问题也没有,但是,问题就出在JIT编译器可能会对这一个过程进行指令重排序操作,当然重排序也是遵循这java内存模型的,不是随便重排的,如果这里按照1 3 2的顺序执行会是什么样呢,线程一此时将singleton指向开辟好的内存,并未完成初始化操作,此时,程序被线程二抢占,由于判断了instance不为null,则会直接返回instance,而此时的instance指向的是一个未初始化的内存空间而已。线程一继续执行,返回的是正常的instace对象。这样出现了所谓的DCL问题,所以要通过volatile关键字来修饰instance,防止指令重排序。
接下来再给大家介绍通过静态内部类实现单例模式,并且也是线程安全的。

public class Singleton {
    private Singleton() {}

    public static Singleton getSingleton() {
        return InnerSingleton.singleton;
    }

    private static class InnerSingleton {
        private static Singleton singleton = new Singleton();
    }
}

    为什么这种方式看起来好像一开始就初始化创建了这个singleton对象,这里就涉及到了java的类加载时机。JAVA虚拟机在有且仅有的5种场景下会对类进行初始化。
1.遇到new、getstatic、setstatic或者invikestatic这4个字节码指令时,对应的java代码场景为:new一个关键字或者一个实例化对象时、读取或设置一个静态字段时(final修饰、已在编译期把结果放入常量池的除外)、调用一个类的静态方法时。
2.使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没进行初始化,需要先调用其初始化方法进行初始化。
3.当初始化一个类时,如果其父类还未进行初始化,会先触发其父类的初始化。
4.当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类。
5.当使用JDK 1.7等动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
    这5种情况被称为是类的主动引用,注意,这里《虚拟机规范》中使用的限定词是"有且仅有",那么,除此之外的所有引用类都不会对类进行初始化,称为被动引用。静态内部类就属于被动引用的行列。这里可以说明,静态内部类的方式是一种延迟加载。为什么说是线程安全呢,《深入理解java虚拟机》中有提到虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕。如果在一个类的()方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行()方法后,其他线程唤醒之后不会再次进入()方法。同一个加载器下,一个类型只会初始化一次。),在实际应用中,这种阻塞往往是很隐蔽的。
这样,就解释了静态内部类实现单例的方式,是属于饱汉模式的,同时也是线程安全的。
    那么如何破坏这种单例模式,拿到多实例呢,下一次再讨论。

你可能感兴趣的:(设计模式,java并发,JVM)