设计模式之单例模式


前言

单例设计模式是接触最早也是相对最简单的设计模式之一,同属于创建型模式,这种设计模式为一个类提供了唯一的一个全局访问对象,并且由该类创建自己的唯一对象。
总体思路就是将需要私有化类构造器,禁止外部创建对象,在类内部提供类级别的静态方法供外部调用,从而获取唯一的对象
在这里插入图片描述


几种单例实现方式

1.饿汉式

public class Singleton {

    private static Singleton instance = new Singleton();

    private Singleton(){
        
    }

    public static Singleton getInstance(){
        return instance;
    }
}

2.懒汉式

public class Singleton {
    private static Singleton instance;
    private Singleton(){

    }
    
    public static Singleton getInstance(){
        if (instance == null){
            instance = new Singleton();
        }
        return instance;
    }
}

饿汉式和懒汉式单例算是老相识了,比较容易理解,他们的区别就是初始化时机不一样,饿汉式随着类加载,单例对象就会被创建,在对象实例真正需要使用前,会造成一定的空间浪费。而懒汉式则是最一开始先空着肚子,即使是类被加载,只要getInstance方法不被调用,单例对象就不会被初始化。

更深层次的问题是,饿汉式是随着类加载而创建,只有一个线程,自然不会有线程安全问题,运行之前,实例已经唯一存在,不会由于多个线程并发访问导致创建多个实例的情况发生。
而懒汉式则会有线程安全问题, 由于对象创建初始化时机是在getInstance方法调用时发生,在并发访问下,自然会出现N个线程同时访问创建出多个实例的可能,此时单例模式不再单例。
一说到线程安全问题,自然联想到同步,是的,对getInstance同步即可

public class Singleton {
    private static Singleton instance;
    private Singleton(){

    }
    
    public static synchronized Singleton getInstance(){
        if (instance == null){
            instance = new Singleton();
        }
        return instance;
    }
}

加锁自然就要面临锁认证带来的性能损耗,以上示例中是对整个方法进行同步,粒度比较大,同步是为了避免多个线程并发执行情况下出现创建多个对象的情况发生,实际真正需要同步的代码只有instance=new Singleton(),这里就可以用同步代码块对这部分代码进行同步,如下

public class Singleton {
    private static Singleton instance;
    private Singleton(){

    }

    public static  Singleton getInstance(){
        if (instance == null){
            synchronized (Singleton.class){
                instance = new Singleton();
            }
        }
        return instance;
    }
}

看上去似乎是可以了,但是实际上是不行的,由于对象的new操作在JVM层面并不是一个原子操作,需要进行很多步骤操作,从宏观上看就有对象创建、对象初始化等步骤,也就是说会出现指令重排序、内存不可见的情况,一个对象的创建过程可以分为
1.分配空间
2.创建实例
3.将引用指针指向对象内存空间
发生指令重排序后,并不一定会严格按照这个流程进行,有可能出现1,3,2的情况,这就导致对象还没有真正的创建完成,引用已经非空,这个时候例外一个线程判断instance == null为假,会直接返回对应的引用,但是这个时候对象还没有完整创建,就会出现问题。
so,为了避免指令重排序问题,加volatile,禁止指令重排序,关于它的原理,在此不赘述

public class Singleton {
        private static volatile Singleton instance;
    private Singleton(){

    }

    public static  Singleton getInstance(){
        if (instance == null){
            synchronized (Singleton.class){
                instance = new Singleton();
            }
        }
        return instance;
    }
}

但是走到这里仔细研究会发现依旧有问题,比如一个线程进入同步代码块后,另外一个线程也来了,发现锁被占用,就会等待锁释放,等第一个线程执行完成退出同步代码块后,释放锁,第二个线程进来,很明显了,对象会被重复创建,所以在instance=new Singleton()之前还需要继续判断一下引用是否为空,这样当第二个线程进来的时候,引用已经非空,条件为假,第二个线程就自己溜达着玩去吧。

public class Singleton {
    private static volatile Singleton instance;
    private Singleton(){

    }

    public static  Singleton getInstance(){
        if (instance == null){
            synchronized (Singleton.class){
                if (instance == null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

这种加锁的方式称之为:双重校验锁(DCL/Double Check Lokcing),即校验两次instance是否为空的条件

总体来看,加锁一定会因为锁的开销而不可避免的带来一些性能损耗,但是相对于整个方法上这种粗粒度锁,明显第二种方案要更好一些,能够优化一部分锁的时间开销。

还有一种通过静态内部类来实现的单例模式

public class Singleton {
    
    private static class InstanceHolder{
        private static volatile Singleton instance = new Singleton();
    }
    private Singleton(){

    }

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

通过一个静态内部类维护单例对象,在外部类提供公共的方法获取实例,由于实例的创建也是与类加载绑定,自然就不存在线程安全的问题,而且由于实例维护在了内部类,内部类不会随着外部类的加载而加载,只有在被访问的时候才会被加载,从而做到了对象实例的延迟加载,避免了内存空间的浪费,总体来看,这种方式要优于前边几种方案。

总结

与其说主题是设计模式,但实际上由于双重校验锁DCL问题来的相对复杂一些,故将这里作为侧重点做了简单的说明。
能力一般,有问题,还望及时指正~

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值