Java - 单例模式与多线程

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java - 单例模式与多线程相关的知识,希望对你有一定的参考价值。

单例模式大家并不陌生,分为饿汉式和懒汉式等。

线程安全的饿汉式单例

饿汉式单例在类第一次加载的时候就完成了初始化,上代码:

public class MyObject {
    private static MyObject myObject = new MyObject();
    public static MyObject getInstance(){
        return myObject;
    }
}

下面来验证饿汉式单例的线程安全性:

public class MyThread extends Thread{
    public void run() {
        System.out.println(MyObject.getInstance().hashCode());
    }
}
public class Test {
    public static void main(String[] args) throws Exception {
        Thread t1 = new MyThread();
        Thread t2 = new MyThread();
        Thread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
    }
}

输出:

763347431
763347431
763347431

三次输出 hashCode 是同一个值,说明饿汉式单例天生就是线程安全的。

结论:饿汉式单例在类第一次加载的时候完成初始化,而且是线程安全的。

 

非线程安全的懒汉式单例

懒汉式单例为延迟加载,调用 getInstance() 时才被创建,上代码:

public class MyObject {
    private static MyObject myObject = null;
public static MyObject getInstance(){ if(myObject == null){ myObject = new MyObject(); } return myObject; } }

这种情况下肯定是非线程安全的。因为判断对象为空和创建对象是一个原子性操作,多线程访问产生竞态条件。(竞态条件:多线程并发中,正确的结果取决于多个线程的执行顺序)

下面做一下验证:

public class MyThread extends Thread{
    public void run() {
        System.out.println(MyObject.getInstance().hashCode());
    }
}
public class Test {
    public static void main(String[] args) throws Exception {
        Thread t1 = new MyThread();
        Thread t2 = new MyThread();
        Thread t3 = new MyThread();
        t1.start();
        t2.start();
        t3.start();
    }
}

输出:

2146509683
810456228
1996534722

hashcode 输出不同,很明显懒汉式单例不具有线程安全性。

结论:懒汉式单例在需要该实例的时候才会进行初始化(仅初始化一次),但却是非线程安全的。在单例类数量少的情况下,这样一个延迟加载和饿汉式单例相比在性能上又没有明显的差距,所以我一般不会选择这种初级的懒汉式单例。

 

基于 volatile 的 DCL 方案为懒汉式单例提供线程安全性

在懒汉式单例的基础上,我们做一些优化使其具有线程安全的特性。

第一次尝试:

为 getInstance() 加上 synchronized 关键字

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

输出

2000544445
2000544445
2000544445

得到的是单例,不过如果多个线程频繁调用 getInstance() 的话将会导致程序执行效率非常低下:下个线程想要取得对象锁,必须等到上一个线程释放锁之后才可以继续执行。

 

第二次尝试:

同步代码块

public class MyObject {
    private static MyObject myObject = null;
public static MyObject getInstance() { synchronized (MyObject.class) { if (myObject == null) { myObject = new MyObject(); } } return myObject; } }

输出

512965639
512965639
512965639

然而这种方法等同于上一种尝试,唯一区别就是多线程下,上一次尝试中线程被堵在门口一米处进不来,而这一次尝试中线程被堵在门口半米处进不来。

 

第三次尝试:

继续缩小同步的代码范围

public class MyObject {
    private static MyObject myObject = null;
public static MyObject getInstance() { if (myObject == null) {
synchronized (MyObject.class) { myObject = new MyObject(); } } return myObject; } }

输出

1175759956
2000544445
2146509683

相比前面几次尝试,执行效率会有显著提升,但是无法得到单例。因为破坏了原子性。

 

第四次尝试:

下面的代码使用了 DCL(双检查锁机制 double check lock )

public class MyObject {
    private static MyObject myObject = null;
public static MyObject getInstance() { if (myObject == null) {
synchronized (MyObject.class) { if (myObject == null) { myObject = new MyObject(); } } } return myObject; } }

输出:

1320194849
1320194849
1320194849

然而此时就是线程安全了吗? 

No~ 不过我给不出反例推到,大家有经验的请指教一下~

 

第五次尝试:

我们还需要为“单例”加上 volatile 关键字。

public class MyObject {
    volatile private static MyObject myObject = null;
    public static MyObject getInstance() {
        if (myObject == null) {
            synchronized (MyObject.class) {
                if (myObject == null) {
                    myObject = new MyObject();
                }
            }
        }
        return myObject;
    }
}

此时才算是线程安全。

 

回头来看,“单例” 为何必须用 volatile 修饰呢 ?

因为关键字 volatile 能够禁止指令重排序。

Example - “单例” 在没有 volatile 修饰的情况下,线程A 和线程B 都是第一次调用该单例方法,线程A 先执行构造方法;然而该构造方法是一个非原子操作,编译后生成多条指令,由于指令重排序,可能会先执行赋值操作(实际是在内存中开辟一片存储对象的区域后直接返回内存的引用),之后 myObject 便不为空了,但是实际的初始化操作却还没有执行,如果就在此时线程B 进入,就会看到一个不为空的但是不完整(没有完成初始化)的对象。而 volatile 关键字能够禁止指令重排序优化,从而安全的实现单例。

 

结论:基于 volatile的 DCL方案能够保留延迟加载的特性,同时赋予了该单例线程安全性。

 

静态内部类实现单例模式

public class MyObject {
    private static class Holder{
        private static MyObject myObject = new MyObject();
    }
    
    public static MyObject getInstance() {
        return Holder.myObject;
    }
}

怎样保证的线程安全? 会不会因为多个线程同时调用 getInstance() 出现指令重排导致初始化不完全呢?

JVM 在加载 class 之后就会进行类的初始化,在类的初始化期间,JVM 会去获取一个初始化锁,这个锁可以同步多个线程对同一个类的初始化,因此指令重排还是可能发生的,但是并不影响获得初始化锁的下一个线程,因为下一个线程进来的时候,上个线程已经完成了类的初始化。

从书上搞了个图过来:

技术分享

 

结论:静态内部类实现的单例模式能够保证线程安全,同时具有延迟加载特性。而且代码够简洁哦。

 

以上是关于Java - 单例模式与多线程的主要内容,如果未能解决你的问题,请参考以下文章

java 单例模式与多线程

java 单例模式与多线程

单例模式与多线程

单例模式与多线程

单例模式与多线程

并发编程:单例与多线程