单例模式详解哦

本文深入探讨了Java中的单例模式,包括饿汉式和懒汉式的实现,以及如何确保线程安全。饿汉式在类加载时即创建实例,适合频繁访问的场景;懒汉式则在首次调用时创建,适用于不常用的数据。为了解决懒汉式的线程安全问题,介绍了双重检查锁定和静态内部类两种方法,以及枚举的单例实现,强调其天然的线程安全和懒加载特性。
摘要由CSDN通过智能技术生成
单例有如下几个特点:
  • 在Java应用中,单例模式能保证在一个JVM中,该对象只有一个实例存在
  • 构造器必须是私有的,外部类无法通过调用构造器方法创建该实例
  • 没有公开的set方法,外部类无法调用set方法创建该实例
  • 提供一个公开的get方法获取唯一的这个实例
单例模式的好处:
  • 某些类创建比较频繁,对于一些大型的对象,这是一笔很大的系统开销
  • 省去了new操作符,降低了系统内存的使用频率,减轻GC压力
  • 系统中某些类,如spring里的controller,控制着处理流程,如果该类可以创建多个的话,系统完全乱了
  • 避免了对资源的重复占用
写法:
  • 饿汉
public class Singleton {
  // 创建一个实例对象
    private static Singleton instance = new Singleton();
    /**
     * 私有构造方法,防止被实例化
     */
    private Singleton(){}
    /**
     * 静态get方法
     */
    public static Singleton getInstance(){
        return instance;
    }
}

之所以叫饿汉式字面意思,就是饿,着急new对象,他想提前把对象new出来,这样别人哪怕是第一次获取这个类对象的时候直接就存在这个类了,省去了创建类这一步的开销。

  • 懒汉
public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  
  
    public static Singleton getInstance() {  
    if (instance == null) {  
        instance = new Singleton();  
    }  
    return instance;  
    }  
}

懒汉式大家可以理解为他懒。就是懒得吃,等别人问他吃了没,他再想想自己吃没吃,别人第一次调用的时候他发现自己的实例是空的,然后去初始化了,再赋值,后面的调用就和饿汉没区别了。

懒汉和饿汉的对比:大家可以发现两者的区别基本上就是第一次创作时候的开销问题,以及线程安全问题(线程不安全模式的懒汉)。

那有了这个对比,那他们的场景好理解了,在很多电商场景,如果这个数据是经常访问的热点数据,那我就可以在系统启动的时候使用饿汉模式提前加载(类似缓存的预热)这样哪怕是第一个用户调用都不会存在创建开销,而且调用频繁也不存在内存浪费了。

而懒汉式呢我们可以用在不怎么热的地方,比如那个数据你不确定很长一段时间是不是有人会调用,那就用懒汉,如果你使用了饿汉,但是过了几个月还没人调用,提前加载的类在内存中是有资源浪费的。

怎么解决懒汉线程安全问题?
  • 1.双重检查
public class Singleton2 {
    private static volatile Singleton2 instance;
    
    private Singleton2(){}

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

用两次判断加同步代码块实现线程安全
用双重检查实现的懒汉式,在多线程场景中getIntance时,首先要进行一次对象是否已创建的判断,如果已创建就直接返回实例,当首次加载需要创建对象时,假设有线程A和线程B两个线程同时通过第一层判断,那么它们需要排队进入同步代码块,假设线程A先进入同步代码块,那么实例由线程A创建,那么当线程B进入同代码块时便不能通过第二层检查,即直接返回实例。这样便实现了线程安全的懒加载。

关于volatile关键字:
volatile有两个作用:保证可见性和防止指令重排
什么是保证可见性呢,就是当一个线程在对主内存的某一份数据进行更改时,改完之后会立刻刷新到主内存。并且会强制让缓存了该变量的线程中的数据清空,必须从主内存重新读取最新数据。意思就是所有线程都可以得到最新的数据,这样一来就保证了可见性。
什么是指令重排呢,当new一个对象时,用字节码指令分析是三条指令(new、dup、invokespecial),这三条指令可能会发生重排序,引用指向分配地址,但对象还未创建,导致判空校验不准确。
因为创建对象的过程都在同步代码块中,所以此处使用volatile的作用主要是保证可见性。

  • 2.静态内部类
public class Singleton3 {

    private Singleton3(){}

    private static class SingletonInstance{
        private static final Singleton3 INSTANCE = new Singleton3();
    }
    
    public static Singleton3 getInstance(){
        return SingletonInstance.INSTANCE;
    }
}

静态内部类:1.当外部类被装载时,内部类并不会被装载 当使用到时才会被装载,且只装载一次。

  • 3.枚举
enum Singleton4 {
    INSTANCE;
    public void method(){
        System.out.println("枚举实现单例");
    }
}

使用:
Singleton4.INSTANCE.method();
枚举是最简单也是最好用的实现方式,枚举的实际是用final修饰的实现enum接口的类,因为枚举构造只能私有,所以枚举是天生的单例模式
因为枚举类是在第一次访问时才被实例化,所以它也是懒加载的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值