创建型设计模式之单例模式

本文详细介绍了Java中的单例模式,包括饿汉式、懒汉式和静态内部类的实现方式。饿汉式在类加载时即创建实例,线程安全但可能导致资源浪费;懒汉式延迟加载,但在多线程环境下可能产生多个实例,需要通过同步锁保证线程安全;静态内部类方式则结合了两者优点,既实现延迟加载,又能确保线程安全。文章还探讨了 volatile 关键字的作用以及类加载过程中的线程安全问题。
摘要由CSDN通过智能技术生成


前言

之前研究了创建型设计模式中的工厂模式,这篇文章学习一下创建型设计模式中的单例模式


一、单例模式

顾名思义,单例模式就是在程序运行过程中,全局只需要一个实例时,就可以使用单例模式,既然单例模式要保证全局唯一,则构造函数必须私有变量被static修饰。这样才可以保证其它类无法实例化该此类,必须通过对应的方法获取唯一示例。单例模式的优点是可以避免对象的重复创建,节省资源。单例模式从创建的时机可以分为饿汉式懒汉式和静态内部类三种。

饿汉式

饿汉式,即变量被声明时即被创建,代码示例如下:

public class HungrySingleton {
    private  static HungrySingleton instance = new HungrySingleton();
    private HungrySingleton(){
        
    };
    public static HungrySingleton getInstance(){
        return instance;
    }
}

从上面代码可以看出,变量在被声明即被创建,饿汉式的优点是线程安全的,但是饿汉式的弊端就是,即使这个单例无论使不使用,在类加载之后也会被立马创建,占用内存并增加类的初始化时间

懒汉式

懒汉式相比饿汉式,区别是,变量声明时并不进行加载,而是实际使用的时候才加载,有效的避免了饿汉式的缺点,最简的饿汉式代码示例如下:

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

    }
    public static UnsafeLazySingleton getInstance(){
        if(instance == null) { // 1
            instance = new UnsafeLazySingleton();
        }
        return instance;
    }
}

上面的代码示例虽然解决了饿汉式的问题,但是是线程不全的,比如现在有线程1和线程2同时执行到step,此时两个线程都会判断成功并进入实例的创建,因此,在多线程的场景下,可能存在示例重复创建的过程,因此要给getInstance方法加锁来保证线程的同步,最常见的懒汉式单例代码如下:

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

    }
    public static LazySingleton getInstance(){
        if(instance == null) { // 1
            synchronized (LazySingleton.class){
                if(instance == null){
                    instance = new LazySingleton();//2
                }
            }
        }
        return instance;
    }
}

这里有两个问题:

  1. 在给类加锁前为什么要先判断instance是否为null。
    sychronized关键字锁住Singleton类而不是instance对象,是因为加锁时如果nstance为空,加sychronized没有任何意义。
    第一重判断的意义在于:如果instance非空,就跳过了锁的步骤,减少加锁的资源开销。但是由于第一重判断在代码锁之外,如果不同线程同时访问到instance==null,会先后阻塞执行代码锁内的内容。所以在代码锁内加第二重判断就很有必要了,避免第一个线程获取实例后,第二个线程获得资源锁又执行了一次instance的初始化,产生两个不同的实例。

  2. 变量前为什么被volatile关键字修饰。
    volatile关键字的一个重要作用是防止指令重排,代码中2的赋值语句并不是一个原子操作(一条指令即可完成),而是可以拆分成以下过程:

    • 分配内存空间。

    • 初始化对象。

    • 将对象指向刚分配的内存空间。

    因为步骤2和步骤3之间并没有数据依赖关系,所以JVM可以对指令进行重排序,也就是可以先进行步骤3后进行步骤2。这会导致另一个线程在获取变量时就会将代码中位置1的条件判断为false,直接返回instance的引用,实际返回的是一个并未初始化完全的实例,会带来线程上的安全问题。

静态内部类

除了饿汉式和懒汉式两种单例方式,还存在另一种单例,静态内部类的方式,代码示例如下:

public class InnerClassSingleton {
    private static class InnerClassSingletonHolder {
        public static InnerClassSingleton instance = new InnerClassSingleton();
    }
    private InnerClassSingleton(){
        
    }
    public InnerClassSingleton getInstance(){
        return InnerClassSingletonHolder.instance;
    }
}

静态内部类的单例模式有两个优点,第一是可以实现懒加载,即懒汉式的优点,第二是可以保证线程安全,下面针对这两点做说明。

  • 实现懒加载
    Java类加载的过程包括:加载、验证、准备、解析、初始化。初始化阶段即执行类的 clinit 方法(clinit = class + initialize),包括为类的静态变量赋初始值和执行静态代码块中的内容。但不会立即加载内部类,内部类会在使用时才加载。所以当此 Singleton 类加载时,SingletonHolder 并不会被立即加载,所以不会像饿汉式那样占用内存。
    另外,Java 虚拟机规定,当访问一个类的静态字段时,如果该类尚未初始化,则立即初始化此类。当调用Singleton 的 getInstance 方法时,由于其使用了 SingletonHolder 的静态变量 instance,所以这时才会去初始化 SingletonHolder,在 SingletonHolder 中 new 出 Singleton 对象。这就实现了懒加载。
  • 线程安全
    Java 虚拟机的设计是非常稳定的,早已经考虑到了多线程并发执行的情况。虚拟机在加载类的 clinit 方法时,会保证 clinit 在多线程中被正确的加锁、同步。即使有多个线程同时去初始化一个类,一次也只有一个线程可以执行 clinit 方法,其他线程都需要阻塞等待,从而保证了线程安全。

总结

如有错误,恳请大家批评指正,日拱一卒,功不唐捐。
参考:
https://www.cnblogs.com/shan1393/p/8999683.html
https://zhuanlan.zhihu.com/p/85624457

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值