设计模式-单例模式

本文详细介绍了Java设计模式中的单例模式,包括饿汉式、懒汉式、双重检查和静态内部类四种实现方式,并分析了各自的优缺点。单例模式在软件开发中用于确保类只有一个实例,提高代码效率和可扩展性。文章特别强调了多线程环境下单例模式的实现注意事项,推荐使用静态内部类方式,因为它既保证了线程安全,又避免了锁的使用。
摘要由CSDN通过智能技术生成

        设计模式在我们的日常开发中会经常设计,在一些场景中选择合适的设计模式,会减少代码的开发量,提升代码的可扩展性,简化后续人员代码的开发难度。在设计模式的这一些列文章中,会对常用的设计模式进行介绍,希望能够提升对设计模式的理解和应用。设计模式主要分为:创建型模式、结构型模式和行为型模式。

        本篇文章属于设计模式的开篇,主要介绍单例这种设计模式,他属于创建型模式。所谓单例,指的是在一个进程中,所使用的该类的实例对象只有一个,也就是该类不管被多少个线程调用,在线程中所使用的对象都是同一个,在我们常用的spring框架中,默认的bean就是单例模式。单例模式的创建有很多种,本篇文章主要介绍四种创建方式:饿汉式,懒汉式,双重检查以及静态内部类。

一 饿汉式

        单例的饿汉式创建方式主要注意以下两点:

        1.构造方法为私有,保证不能通过new的方式进行创建;

        2.对外提供静态的实例获取方法

        具体的代码如下,这种单例模式是最简单并且安全的一种单例创建方式,但是它存在一个缺点就是,当类加载完毕以后,不管该类有没有在项目中被使用,他都会被实例化,占用内存。如果能够保证该类是一个经常被使用的单例类,那么就可以通过下面的这种方式进行创建。

/**
 *  饿汉式单例模式,类加载到内存后,就会进行实例化,不管是否被使用,都会直接实例化
 */
public class Single_A {
    // 由于该变量未静态变量,那么在类加载的时候就会被初始化,进行对象的创建
    private static final Single_A INSTANCE = new Single_A();
    
    // 构造方法私有,无法直接被外界创建
    private Single_A(){}

    // 外界获取该单例实例的静态方法
    public static Single_A getInstance(){
        return INSTANCE;
    }
}

二 懒汉式

        懒汉式创建方式其实就是只有被使用的时候,才会去创建该单例类的实例,可以避免上面饿汉式的缺点。它和饿汉式的唯一区别就是,只有在外部调用获取实例的时候才会去进行实例化。

        但是需要注意的是,下面这种代码编写方式是最简单的懒汉式创建方式,它在单线程的情况下是没有任何问题的,但是如果是在多线程的情况下,下面这种代码编写方式是不安全的。在以前的的jvm相关文章中介绍过对于new Single_B()这个操作在字节码层面它并不是一步操作,它大致可以分为三步:1)分配空间;2)参数初始化;3)将引用指向内存空间。在底层执行这些字节码指令的时候,在不影响最终结果的前提下,是可以进行充排序的。也就是上面的步骤有可能为1->3->2,我们可以假设现在有两个线程,A和B,现在A已经执行到new Single_B(),按照1->3->2执行步骤执行到3,B执行到了if判空,此时B判断为空还是成立,则会继续执行if中的逻辑,如果是这种情况,A和B拿到的实例对象则不是同一个。
 

/**
 * 懒汉式单例,在使用时进行实例化,但是下面这段代码存在线程安全问题
 * 在单线程的情况下是没有问题的
 */
public class Single_B {
    private static Single_B INSTANCE;
    private Single_B(){}
    public static Single_B getInstance(){
        if (Objects.isNull(INSTANCE)){
            return new Single_B();
        }
        return INSTANCE;
    }
}

三 双重检查

        其实双重检查相当于是对懒汉式的一种优化,解决了上面那种懒汉式的线程安全问题。其实有一种比较简单的解决方式,是对整个getInstance方法加锁,但是这种整体加锁的方式的锁粒度会比较粗(在上面的代码中可能是只存在一个new没有其它的逻辑,影响不是很大,在实际的业务场景中,可能存在其它的业务实现,所以尽量减小锁粒度)。在进行一系列优化以后,就出现了下面的这种创建方式。

        下面这段代码相比于普通的懒汉式主要有以下两个不同的处理方式:

        1.使用volatitle关键字修饰成员变量,在前面的文章中有过介绍,该关键字能够保证变量的可见性以及不可重排序,在这里的作用主要是不可重排序,也就是在执行new Single_B()只能按照1->2->3这种顺序执行,不会再进行指令重排;

        2.在getInstance方法中添加锁处理,并且在加锁的前后都判断了对象是否为空,外面的if判断是不能去除的,因为加锁是一个比较重量级的操作,外面的判断可以减少加锁操作。那么锁里面的if判断能够省略吗?答案是不能,假设两个线程A和B,A经过第一层if判断以后,开始进行加锁,在A加锁的过程中,B也通过了第一层if判断,此时B等待A释放锁,A开始进行对象创建,然后释放锁,如果没有第二层if判断,B就会直接进行对象创建,这样A和B拿到的对应就不是同一个对象,所以这两层if判断都不能去除。

/**
 * 双重检查
 *  
 */
public class Single_C {
    private volatile static Single_C INSTANCE;

    private Single_C(){}

    public static Single_C getInstance(){
        // 第一层if判断,用来减少加锁操作
        if (Objects.isNull(INSTANCE)){
            // 进行加锁,保证线程安全
            synchronized (Single_C.class){
                // 第二层if判断,用来保证在加锁的过程中,通过第一层if判断的线程,拿到的也是同一个对象
                if (Objects.isNull(INSTANCE)){
                    INSTANCE = new Single_C();
                }
            }
        }
        return INSTANCE;
    }
}

四 静态内部类

        上面这种双重检查的方式,虽然解决了饿汉式在类加载的时候就进行实例化的缺点,也解决了普通懒汉式的线程安全问题,但是它引入了锁,并且代码编写相对而言比较复杂。下面这种静态内部类的方式可以说,很完美的解决了上面那三种方式的问题。

        在类进行加载的时候,是不会加载其内部类的,每个类只会被加载一次,由jvm虚拟机保证其单例性。具体实现看下面的代码。

/**
 * 通过静态内部类
 * 加载外部类的时候不会加载内部类
 * JVM保证单例,因为一个类加载的时候只会加载一次
 */
public class Single_D {

    private Single_D(){}

    private static class Single_D_Holder {
        private final static Single_D INSTANCE = new Single_D();
    }

    public static Single_D getInstance(){
        return Single_D_Holder.INSTANCE;
    }
}

其实还有一种单例模式,通过枚举的方式创建单例,他可以防止反序列化,代码实现如下:

public enum Single_E {
    INSTANCE;
}

当然在进行单例的使用的时候,根据具体的情况进行选择,如果你的类就是一个保证使用的类,那么就可以使用饿汉式的方式就行了,既简单又方便。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值