单例模式饿汉模式与懒汉模式

目录

        1.什么是单例模式

2.为什么需要单例模式? 

3.如何实现单例模式

3.1饿汉方式

3.2懒汉模式


1.什么是单例模式

单例模式是一种设计模式,单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例。单例模式的具体实现又分为饿汉模式和懒汉模式两种。

2.为什么需要单例模式? 

有的类比较庞大和复杂,它的实例对象的创建和销毁对资源来说消耗很大,如果频繁的创建和销毁对象,并且这些对象是完全可以复用的,那么将会造成一些不必要的性能浪费。

3.如何实现单例模式

单例模式具体的实现方式分为“饿汉” 和 “懒汉” 两种方式

在实现单例模式时需要考虑两点:

        1、是否线程安全

        2、是否懒加载(不知道什么是懒加载的懒汉方式会解释到)

3.1饿汉方式

        代码实现:

public class Singleton {
    private Singleton(){}//构造私有构造方法
    private static Singleton instance = new Singleton();//创建私有属性对象
    //通过getInstance提供公开对象
    public static Singleton getInstance(){
        return instance;
    }
}

饿汉模式线程是安全的,但是却不是懒加载的。

3.2懒汉模式

(1)非线程安全版

public class Singleton2 {
    private Singleton2(){}//构造器私有
    private static Singleton2 instance = null;//初始化实例对象
    public static Singleton2 getInstance2(){
        if(instance == null){
            instance = new Singleton2();
        }
        return instance;
    }
}

首先将构造器设置为private,那么其他类就无法通过new来直接构造这个分构成对象实例了,而其他类中需要使用Singleton2对象的话只能通过调用getInstance方法,在getInstance方法中,首先判断instance是否被构造过,如果构造过就直接使用,如果没有就当场构造。

懒加载:实例对象是第一次被调用的时候才真正构建的,而不是程序一启动它就够建好了等你调用的,这种滞后构建的方式就叫做懒加载。

在程序中,“懒” 是个好习惯,并且我们要尽可能的 “懒” 。那么懒加载的好处是什么?因为有的对象构建开销是比较大的,若该项目从项目启动就构建,万一从来都没被调用过,就会产生浪费,只有当真正需要使用了再去创建,才是更加合理的。

不难看出这段代码线程是不安全的,因为此时是并发执行状态,在执行if(inatance == null)时,可能会有多个不同的线程同时进入,这样就会实例化多次。

比如:

public class demo02 {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            System.out.println(Singleton2.getInstance2());
        });
        Thread t2 = new Thread(() -> {
            System.out.println(Singleton2.getInstance2());
        });
        Thread t3 = new Thread(() -> {
            System.out.println(Singleton2.getInstance2());
        });

        t1.start();
        t2.start();
        t3.start();
    }
}

运行结果:

可以看出产生了两个实例,而本应该只产生一个,所以线程是不安全的,因此这种方式不可取,需要加以改进。

(2)线程安全版改进版1

public class Singleton3 {
    private Singleton3(){}
    private static Singleton3 instance = null;//初始化实例对象
    public static synchronized Singleton3 getInstance3(){//通过加锁使其只能进入一个线程
        if(instance == null){
            instance = new Singleton3();
        }
        return instance;
    }
}

想要在多线程状态下使其安全运行,我们可以通过加锁synchronized来改进第一版,这样getInstance3()方法同一时间只能进入一个线程,就可以保证线程安全。

但是这样又引入了新的问题,其实我们只想要对象在构建的时候同步线程,而这样的话每次在获取对象时就都要进行同步操作,对性能影响非常大,无疑是捡了芝麻露了西瓜,所以这种写法大多数情况下都不可取。

(3)线程安全版改进版2

public class Singleton4 {
    private Singleton4(){}
    private static Singleton4 instance = null;//初始化实例对象
    public static Singleton4 getInstance4(){
        if(instance == null){
            synchronized (Singleton4.class){//此处进行加锁
                instance = new Singleton4();
            }
        }
        return instance;
    }
}

在改进版中,现在getInstance是不需要竞争锁的,所有线程都可以直接进入,此时进行第二步判断,如果实例对象还没有构建,那么多个线程开始争抢锁,抢到手的线程开始创建实例对象,实例对象创建之后,以后所有的线程在执行到if判断时都可以直接跳过,返回实例对象来进行调用,这就解决了上一版的低效问题。

测试结果:

但是这段代码还是有些问题的,根据测试结果,我们发现,在多个线程执行if判断后,虽然只有一个线程能够抢到锁去执行if内部的代码,但是可能会有其他线程已经进入了if代码块,此时正在等待,一旦线程a执行完,线程b就会立即获取锁,然后进行对象创建,这样对象就会被创建多次。

 (4)线程安全版改进最终版

public class Singleton5 {
    private Singleton5(){}
    private static volatile Singleton5 instance = null;//使用volatile关键字初始化实例对象
    public static Singleton5 getInstance5(){
        if(instance == null){//第一层加锁保证线程效率
            synchronized (Singleton5.class){
                if(instance == null){//第二层加锁保证线程安全
                    instance = new Singleton5();
                }
            }
        }
        return instance;
    }
}

我们可以给上一版的锁外面再加一个if判断,这样就保证了我们的线程既是安全的又是高效的。

为什么要使用volatile关键字?

  因为 instance == new Singleton() 在指令层面,这不是一个原子操作,他分为了三步:     

  1. memory = allocate() //分配内存
  2. ctorInstanc(memory) //初始化对象
  3. instance = memory //设置instance指向刚分配的地址

  在真正执行时,虚拟机为了效率可能会进行指令重排序,比如先执行第一步,再执行第三步,再执行第二步,如果按照这个顺序,a线程执行到了第三步时,此时instance还未被初始化,假设在此时b线程执行到了 if(instance == null) 这一步,此时在b线程中, instance == null 返回false,直接跳过,但是a线程内的instance还未初始化,就会导致b线程中调用 getInstance() 未初始化,出现线程不安全的情况。因此给instance 加上 volatile 修饰,就可以阻止作用在 instance 上的指令重排序问题

我们可以假设a b 两个线程同时进入getInstance()方法,a首先获取了锁然后进行了instance的构建,当它构建完之后它会交还锁,这时候b线程也会立即获得锁,在获得锁之后进行一个判空,此时我们可以看到instance因为已经被线程a初始化了,所以instance是不等于null的,于是b线程将会直接退出,返回实例,这样就不会造成线程不安全的问题了,这种对对象进行两次判空的操作叫做双重校验锁。

该篇博客借鉴了b站up主寒食君的视频讲解。

理解有限,如有不足,还望指出!!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值