天天看点

单例模式以及其中的线程安全问题1. 饿汉式2. 懒汉式3. 如何解决懒汉式中存在的线程安全问题?4. new对象的操作中的指令重排序问题5. 推荐使用饿汉式

有一些类,在内存中没有必要存在多个对象。这时候就出现了单例模式。

1. 饿汉式

使用

static

保证了线程安全,在类加载到内存的时候,进行实例化。

/**
 * 饿汉式
 * 类加载到内存后,就实例化一个单例,JVM保证线程安全
 * 简单实用,推荐使用!
 * 唯一缺点:不管用到与否,类装载时就完成实例化
 * Class.forName("")
 * (话说你不用的,你装载它干啥)
 */
public class Mgr01 {
    private static final Mgr01 INSTANCE = new Mgr01(); 

    private Mgr01() {};

    public static Mgr01 getInstance() {
        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        Mgr01 m1 = Mgr01.getInstance();
        Mgr01 m2 = Mgr01.getInstance();
        System.out.println(m1 == m2);
    }
}
           

2. 懒汉式

有人说上面的饿汉式,我都还没有用,你就给我创建了一个对象,能不能在我用的时候再创建对象?于是又有了懒汉式。在调用

getInstance

方法的时候,才去创建对象,而且创建之前先判断是不是为空。

单线程环境下,这段代码确实没有问题。但是多线程情况下,就会有问题。看代码中的注释(很好理解的)。

/**
 * lazy loading
 * 也称懒汉式
 * 虽然达到了按需初始化的目的,但却带来线程不安全的问题
 */
public class Mgr03 {
    private static Mgr03 INSTANCE;

    private Mgr03() {
    }

    public static Mgr03 getInstance() {
        if (INSTANCE == null) { // 一个线程过来了,判断了,INSTANCE 是null。这时候又有一个线程过来了,
        // 也判断了INSTANCE 是null。然后第一个线程继续执行,创建了一个对象。接着第二个线程继续开始执行,
        //也会创建一个新的对象(第二个线程已经执行过判断INSTANCE 是不是null的操作)。这时候就不能保证单例了。
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new Mgr03();
        }
        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        for(int i=0; i<100; i++) {
            new Thread(()->
                System.out.println(Mgr03.getInstance().hashCode())
            ).start();
        }
    }
}
           

3. 如何解决懒汉式中存在的线程安全问题?

  1. getInstance

    方法上加个锁不就行了

    确实能够达到目的,但是又有人说了,整个函数加锁,有效率问题。能不能将锁细化?于是又有了第

    2

    种方案。
/**
 * lazy loading
 * 也称懒汉式
 * 虽然达到了按需初始化的目的,但却带来线程不安全的问题
 * 可以通过synchronized解决,但也带来效率下降
 */
public class Mgr04 {
    private static Mgr04 INSTANCE;

    private Mgr04() {
    }

    public static synchronized Mgr04 getInstance() {
        if (INSTANCE == null) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new Mgr04();
        }
        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        for(int i=0; i<100; i++) {
            new Thread(()->{
                System.out.println(Mgr04.getInstance().hashCode());
            }).start();
        }
    }
}
           
  1. 加锁之前,先判断

    instance

    是不是

    null

    ,是

    null

    然后才加锁。

乍一看,好像挺好的,没啥问题。但是这种写法也是有线程安全问题的(参考方法中的注释)。

/**
 * lazy loading
 */
public class Mgr05 {
    private static Mgr05 INSTANCE;

    private Mgr05() {
    }

    public static Mgr05 getInstance() {
        if (INSTANCE == null) {  //  一个线程是来了,判断INSTANCE 是null,这时候第二个线程来了,
        //也判断了INSTANCE 是null。线程二获得了锁,然后创建了对象,执行完释放了锁;第二个线程获得锁,
        //继续执行,也会创建一个新的对象(因为它没有再次判断INSTANCE 是不是null)。这不就有两个对象了吗?
            //妄图通过减小同步代码块的方式提高效率,然后不可行
            synchronized (Mgr05.class) {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                INSTANCE = new Mgr05();
            }
        }
        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        for(int i=0; i<100; i++) {
            new Thread(()->{
                System.out.println(Mgr05.getInstance().hashCode());
            }).start();
        }
    }
}
           
  1. 加锁之后,再判断

    INSTANCE

    是不是

    null

    上面的代码是由于加锁之后,没有判断

    INSTANCE

    是不是

    null

    导致的。那简单,加锁之后再判断一下

    INSTANCE

    是不是

    null

    不就解决了吗?

    这就引出了单例模式

    Double Check Lock

    的写法。也就是

    getInstance

    方法里面,加锁之前和之后分别检查下

    INSTANCE

    是不是

    null

    没问题了吗?注意代码中的

    volatile

    是注释掉的。

    这就是一个面试题了。单例模式中的

    DCL

    写法,实例变量是否需要加

    volatile

    以及为什么?
/**
 * lazy loading
 */
public class Mgr06 {
    private static /*volatile*/ Mgr06 INSTANCE; //JIT

    private Mgr06() {
    }

    public static Mgr06 getInstance() {
        if (INSTANCE == null) {
            //双重检查
            synchronized (Mgr06.class) {
                if(INSTANCE == null) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    INSTANCE = new Mgr06();
                }
            }
        }
        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        for(int i=0; i<100; i++) {
            new Thread(()->{
                System.out.println(Mgr06.getInstance().hashCode());
            }).start();
        }
    }
}
           

4.

new

对象的操作中的指令重排序问题

要回答单例模式中的

DCL

写法,实例变量是否需要加

volatile

以及为什么?这个问题,首先得要清楚

new

一个对象的操作其实是分为三条指令的。

分别是申请内存赋默认值、初始化、将实例变量指向对象。

这三条指令是可能发生指令重排序的,初始化操作和将将实例变量指向对象的操作的顺序会互换。这时候就会出现线程安全的问题。

第一个线程来了,判断

INSTANCE

null

,然后加锁,进入了

new

对象的过程,如果发生指令重排序(先对实例变量进行了赋值操作),在

new

到一半的时候,

INSTANCE

已经被赋值,这时候,第二个线程来了,判断

INSTANCE

不是

null

,会直接返回还没有初始化完成的

INSTANCE

对象,就会出现问题。

volatile

禁止指令重排序。就可以解决上述问题。

单例模式以及其中的线程安全问题1. 饿汉式2. 懒汉式3. 如何解决懒汉式中存在的线程安全问题?4. new对象的操作中的指令重排序问题5. 推荐使用饿汉式

5. 推荐使用饿汉式

其实工作中使用饿汉式就够了,没必要搞得这么复杂。所谓面试造火箭,工作拧螺丝。。。。内卷的厉害。

继续阅读