你不知道的单例模式讲解,涉及线程安全问题,面试单例模式这一篇就够了

什么是单例模式?

单例模式是二十三种设计模式的一种,也是面试中常问到的一种,其提供了多种实现方案,包括,饿汉式,懒汉式,双重检查,静态内部类,枚举类实现方式,其中,饿汉式和懒汉式是最常用的,其中也涉及到线程安全的问题.

饿汉式普通版本

  1. 必须构造器私有化,防止在外部被实例化.
  2. 实例化必须在类的内部.
  3. 向外暴露一个接口方法,返回在类内部创建的对象.
    在这里插入图片描述

这是尚硅谷代码的例子,可以看到,我们创建对象的时候直接在静态的私有属性里创建了,然后通过外部接口返回了该对象.

这样做的好处是简单,且安全!如果你了解jvm就知道,静态属性是在类装载的时候就加载了,这里说明在装载的时候完成了对象的实例化,肯定避免了线程同步的问题.

但是问题也随之而来,这样一上来就创建了对象,后面对于我们的业务要不要使用这个对象是没有任何关系的,也就是说,该对象是在类装载的过程就完成了实例化,没有达到懒加载的效果(没有达到我们想用就创建对象的效果),万一后面我们根本就不用该对象,就白白创建了该对象,造成了内存浪费.

其中,还有个静态代码块的版本,就是在静态代码块中完成实例化的操作,但是这样的效果和上面的差不多,这里不再赘述.

懒汉式普通版本

懒汉式就对应饿汉式,饿汉式一上来就创造了对象,懒汉式是符合懒加载机制的,也就是当我们想用的时候就创建,不用的时候就不创建.

在这里插入图片描述
从上面的代码分析,如果有多个线程同时进入了创建对象的if语句中,那么就会返回多个实例,虽然判断了该对象等于null的时候才去创建的,理论上就创建了一个对象,因为之后的这个对象不可能为空了,但是这只是符合单线程的场景,多线程下肯定是不行的.

所以说,这里的优点是:符合懒加载机制,避免了上面的内存浪费的问题
但缺点是: 只能在单线程下使用,多线程下会产生同步问题,创建出多个实例.

懒汉式线程安全问题

  1. 首先,线程安全问题是很好解决的,相信很多小伙伴都可以猜到,我们直接在方法上加上同步的关键字就可以了.
    在这里插入图片描述
    但是我们都知道这个同步关键字效率是有些低的,所以我们还可以将此关键字更深入一些,提升到同步代码块的等级,锁住整个类:
    在这里插入图片描述
    但是你想想,这个真的是单例的吗?

我们先分析那个if判断,如果该实例为空,就进来,然后该线程加载同步关键字,进行new一个实例,感觉良好,因为同步关键字,防止了同步问题? 答案肯定是错的,同步关键字确实可以防止同步问题,但是在这里不同,还得在同步代码块里加上一个同样的if判断,也就是if(singleton==null)操作.这就是双重校验法.

双重校验法

我们继续分析上面的问题.
假如有两个线程同时进入了第一个if判断(因为if这里没有解决同步问题,是可以进入的),但是因为同步代码块,只有一个线程在某一刻进行实例化了对象,但是因为这两个线程是同时进入第一个if判断的,另外一个线程在第一个线程实例化完后也会进入到同步代码块中,这个时候也会创建一个实例对象.

你第一个if语句不可能加上同步关键字,那岂不是效率更低了,所以你只能在同步代码块中加入一个同样的判断,这个时候,如果有两个线程同时进入了第一个if判断,当这些线程在不同时间执行同步代码块的时候,就会因为代码块里的if语句判断,只创建出一个对象

在这里插入图片描述
相信很多小伙伴会问: 这里为什么使用volatile变量?
那你肯定得知道,volatile是干什么的.首先,volatile在此处的作用是防止指令重排,至于什么是指令重排,就是jvm底层为了提高代码执行效率的一个机制,会将代码执行顺序给调换,这样在单线程是没有问题的,但是多线程因为变量是共享的,就会出现问题,简单来说就是,有可能一些代码变量还未加载到,已经有线程使用它了.具体详细可以网上查.

静态内部类的方式

在这里插入图片描述
这里有点像上面那个静态代码块和静态属性的方式创建,但是上面是有内存浪费的风险,这里就不会存在的,而且是推荐使用的.

优点: 优先,这是线程安全的,因为静态关键字的属性,是在类装载的过程保证实例化的.其次,通过使用静态内部类的方式进行实例化,不会立马的去实例化,只是在我们实例化该对象的时候才会被实例化,实现了延迟加载.

枚举类型

我们先考察上面所列举的实现方式,有线程安全的,有线程不安全的,但这是线程问题,我们还得考虑反序列化和反射等问题,如果对上面的类进行反序列化,还是可以创建多个对象的,而且,通过反射,也可以创建多个对象,不管你是不是静态的属性,还是私有属性,内部类等等.

那如果是上面的实现方式,我们保证被反序列下还能创建一个新的实例呢?在每次序列化的时候都会创建一个新的实例,为了保证只创建一个实例,必须声明所有字段都是 transient,并且提供一个 readResolve() 方法

但是这也解决不了反射攻击.

所以可以使用枚举类,这样很安全,枚举可以避免线程安全问题,和反序列化问题,还有反射攻击!这也是Effective Java作者Josh Bloch 提倡的方式,大佬都提倡了,所以建议都使用这种方式!

下面是实现方式:
在这里插入图片描述

单例模式的使用场景

需要频繁的进行创建和销毁的对象、创建对象时耗时过多或
耗费资源过多(即:重量级对象),但又经常用到的对象、工具类对象、频繁访问数
据库或文件的对象(比如数据源、session工厂等)

在JDK中,java.lang.Runtime就是经典的单例模式(饿汉式

所有代码

package com.hyb.ds;

class A {

    private A() {
    }

    private static A a=new A();

    public static A getA(){
        return a;
    }

}

class B{
    private B(){

    }

    private volatile static B b=null;

    public static B getB(){
        if (b==null){

            synchronized (B.class){
                if (b==null){
                    b=new B();
                }
            }
        }
        return b;
    }
}

class C{
    private C(){}

    private static class D{
        private static final C c=new C();
    }
    public static C getC(){
        return D.c;
    }
}


class EnumSingleton {
    private EnumSingleton() {

    }

    public static EnumSingleton getInstance() {
        return Singleton.INSTANCE.instance;
    }

    private enum Singleton {
        INSTANCE;

        private final EnumSingleton instance;

        //JVM保证只执行一次
        Singleton() {
            instance = new EnumSingleton();
        }
    }

}


  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值