单例模式以及其中的线程安全问题

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

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 一个对象的操作其实是分为三条指令的。
分别是申请内存赋默认值、初始化、将实例变量指向对象。
这三条指令是可能发生指令重排序的,初始化操作和将将实例变量指向对象的操作的顺序会互换。这时候就会出现线程安全的问题。

第一个线程来了,判断INSTANCEnull,然后加锁,进入了new对象的过程,如果发生指令重排序(先对实例变量进行了赋值操作),在new到一半的时候,INSTANCE 已经被赋值,这时候,第二个线程来了,判断INSTANCE 不是null,会直接返回还没有初始化完成的INSTANCE 对象,就会出现问题。

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

在这里插入图片描述

5. 推荐使用饿汉式

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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
单例模式可以通过多种方式来保证线程安全。其一种常见的方式是使用懒汉式实现。在懒汉式,当实例没有被创建的候,如果有多个线程同调用getInstance方法,可能创建多个实例,从而导致线程安全问题。为了解决这个问题,可以在getInstance方法上使用synchronized关键字进行加锁,这样可以保证在同一间只有一个线程可以访问该方法,从而避免了多个实例的创建。然而,由于synchronized的粒度较大,影响性能。因此,可以采用双重校验锁+volatile关键字的方式来提高性能。双重校验锁可以在实例创建之前进行一次判断,避免了不必要的加锁操作。而volatile关键字可以保证变量的可见性,防止指令重排序,从而实现线程间变量修改的可见性。需要注意的是,volatile关键字并不具备原子性,因此不能单独使用它来实现线程安全。\[1\]\[2\]\[3\] #### 引用[.reference_title] - *1* *3* [单例模式-线程安全的5种实现](https://blog.csdn.net/murongyeye/article/details/111378319)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [单例模式线程安全问题](https://blog.csdn.net/qq_58710208/article/details/123954636)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值