单例模式和双重检测锁模式下的相关问题

单例模式

单例模式要点

​ 其实单例模式重点有三个:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。从实现角度来说,就是以下三点:一是单例模式的类只提供私有的构造函数,二是类定义中含有一个该类的静态私有对象,三是该类提供了一个静态的公有的函数用于创建或获取它本身的静态私有对象。

方式一:饿汉模式

顾名思义,很饿,没见过吃的,一开始就要吃。其实就在在启动的时候就创建对象,这种方式比较的消耗内存,不太推荐在内存消耗较大的对象上面使用,好处是一开始就创建对象,没有多线程下的安全问题。

public class Hungry {
    //私有构造方法,防止自行实例化对象
    private Hungry() {
    }
    
    //静态属性,在一开始就创建是实例对象
    private final static Hungry HUNGRY = new Hungry();

    //通过这个方法拿是实例对象
    public static Hungry getInstance() {
        return HUNGRY;
    }

}

方式二:懒汉模式

顾名思义,很懒,不火烧眉毛不动手。这种模式只会在你使用对象的时候才创建对象,这种方式避免一开始就使用过多的内存,推荐在内存消耗较大的对象上面使用。

public class LazyMan {
    //volatile->禁止指令重排,避免创建实例对象时重排指令空指针异常
    private volatile static LazyMan lazyMan;

    // 双重检测锁模式的 懒汉式单例  DCL懒汉式(double check lock)
    public static LazyMan getInstance() {
        if (lazyMan == null) {
            synchronized (LazyMan.class) {//对LazyMan上锁,同一时间只能有一个线程能够进入创建对象
                if (lazyMan == null) {
                    lazyMan = new LazyMan(); // 不是一个原子性操作
                }
            }
        }
        return lazyMan;
    }

解释:

  1. 为啥使用synchronized?

    答:在多个线程进入getInstance()方法时,如果不加入synchronized锁住LazyMan.class,可能会有对个线程成功实例化对象,这就会违背单例模式的初衷,当加上以后,因为锁住的是class,所以只会有一个线程能够拿到锁来实例化对象,保证单例.

  2. 又为啥使用volatile?

    答:如果看过编译原理,其实里面有一个对编译器的介绍中,编译器会根据需要将代码的执行顺序进行优化,类比这里,我们在实例化一个对象的时候,可能是这样的顺序:为1.lazyMan分配内存空间->2.初始化lazyMan->3.为lazyMan指向第一步中分配的内存空间。但是在编译器的好心下,我们的执行顺序变成了1->3->2,那么这就出现问题了,我们的其他的线程拿对象会到哪里去拿?回到第一步分配的内存空间去拿!假如现在实例对象的线程正好按着1->3->2的顺序刚刚执行完了3,我们另一个线程过来了,因为2还没有执行,内存地址对应的内存空间中还没有东西,那么我们拿的就是个空,但是这个时候这个地址确实是被分配了的。这个时候就会出现空指针异常,也就是因为实例化对象的时候不满足原子性。

  3. 那又又为啥用两个if (lazyMan == null)

    答:好问题!第一个外层的好解释,在我们成功实例化对象之后,我们还需要进入加锁的哪一步吗?其实是不需要的,我们就可以直接返回对象去用就好了。那么第二个内层的又是用来干嘛的?在我们未创建实例对象的时候,我们假设有一千个线程来创建这个实例对象,我们一号线程拿到了锁,二号线程撞到了门上,没拿到锁,于是它在门口等,此时注意二号线程已经经过了第一个if (lazyMan == null),然后线程一执行完了实例,然后返回了这个实例对象LazyMan2@7f1d78ac,这个时候线程二啪的一下拿到了线程一扔下的锁,很快啊!然后又因为没有第二层的if (lazyMan == null),他又实例化了一个对象LazyMan2@688ee48d,然后线程二开心的返回了。此时我们的实例对象已经是线程二的LazyMan2@688ee48d,这个时候第三个线程来了,它在第一层的if (lazyMan == null)被告知对象已经实例化了,拿走去用吧,后来的都是像线程三一样被安排的明明白白。从此以后就只有一个对象LazyMan2@688ee48d,但是这个是正常的吗。我们也违反了单例模式的原则。所以需要加上第二层的if (lazyMan == null)。

方式三:静态内部类

静态内部类的方式在开始的时候没有实例化对象,在第一次在调用getInstace时才会第一次实例化Holder。使用的classloader 机制来保证初始化 instance 时只有一个线程。和双检锁是一样的功效,实现更加的简单。但是这种方法不是很好传递参数,所以如果实例对象是固定的可以使用这种方式。

// 静态内部类
public class Holder {
    //构造器私有
    private Holder() {

    }

    public static Holder getInstace() {
        return InnerClass.HOLDER;
    }

    //    静态内部类,在调用getInstace时,才会第一次实例化Holder。
    public static class InnerClass {
        private static final Holder HOLDER = new Holder();
    }
}

方式四:枚举类型

这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。

public enum EnumSingle {

    INSTANCE;

    public EnumSingle getInstance() {
        return INSTANCE;
    }
}

总结:

单例模式在面试的时候问的比较多,希望对大家有帮助。

参考书籍《Effective Java中文版(原书第3版)》

近期文章
SpringBoot整合Redis及简单使用
Docker安装Mysql以及Mysql的基本操作——入门必看
vue-cli十分钟学习入门笔记――开袋即食
如何判断2的n次方?用四种方式来扒一扒。
关于SpringAOP的三种实现方式你有了解过吗?
八皇后问题详细另类图解-九张图带你了解什么是八皇后问题
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值