单例模式(设计模式)

发布于:2024-04-27 ⋅ 阅读:(26) ⋅ 点赞:(0)

概述

单例模式:单例对象能保证在一个JVM中,该对象只有一个实例存在。保证被创建一次,节省系统开销
解决的问题:保证一个类在内存中的对象唯一性
  所谓单例,指的就是单实例,有且仅有一个类实例,这个单例不应该由人来控制,而应该由代码来限制,强制单例。
  单例有其独有的使用场景,一般是对于那些业务逻辑上限定不能多例只能单例的情况,例如:类似于计数器之类的存在,
一般都需要使用一个实例来进行记录,若多例计数则会不准确。
  其实单例就是那些很明显的使用场合,没有之前学习的那些模式所使用的复杂场景,只要你需要使用单例,那你就使用单例,简单易理解。
  所以我认为有关单例模式的重点不在于场景,而在于如何使用。

懒汉式

何为懒?顾名思义,就是不做事,这里也是同义,懒汉式就是不在系统加载时就创建类的单例,而是在第一次使用实例的时候再创建。
详见下方代码示例:

public class LHanDanli {
    //定义一个私有类变量来存放单例,私有的目的是指外部无法直接获取这个变量,而要使用提供的公共方法来获取
    private static LHanDanli dl = null;
    //定义私有构造器,表示只在类内部使用,亦指单例的实例只能在单例类内部创建
    private LHanDanli(){}
    //定义一个公共的公开的方法来返回该类的实例,由于是懒汉式,需要在第一次使用时生成实例,所以为了线程安全,使用synchronized关键字来确保只会生成单例
    public static synchronized LHanDanli getInstance(){
        if(dl == null){
            dl = new LHanDanli();
        }
        return dl;
    }
}

饿汉式

又何为饿?饿者,饥不择食;但凡有食,必急食之。此处同义:在加载类的时候就会创建类的单例,并保存在类中。
详见下方代码示例:

public class EHanDanli {
  //此处定义类变量实例并直接实例化,在类加载的时候就完成了实例化并保存在类中
   private static EHanDanli dl = new EHanDanli();
   //定义无参构造器,用于单例实例
  private EHanDanli(){}
 //定义公开方法,返回已创建的单例
   public static EHanDanli getInstance(){
       return dl;
    }
 }

双重加锁机制

在懒汉式实现单例模式的代码中,有使用synchronized关键字来同步获取实例,保证单例的唯一性,但是上面的代码在每一次执行时都要进行同步和判断,
DCL是一种单例模式写法的简称,全称是Double Check Lock,翻译过来叫双重检查锁。从命名上来理解,
就是两次检查加一把锁。那么,两次检查又是检查什么,锁又是锁的什么?
从代码中,我们发现两次检查的判断条件都是 null == instance,而且两个检查条件是嵌套的。在第1次检查条
件的代码块中,加了一段synchronized代码块,synchronized就是锁。
相当于,不管单例对象是否已经创建,每次调用都可能阻塞,会影响程序的执行效率。所以,加上第1次检查的
目的是,保证只有第一次出现并发的情况会阻塞,提高性能。
因此,第2次检查的目的是,保证单例,避免重复创建单例对象。
第1次检查是为了保证只有首次并发的情况下才阻塞,提高性能,
第2次检查是为了保证,避免重复创建对象。加锁,当然就是为了保证线程安全。
在今天的分享,我还有一个细节没有讲到,就是在并发情况下,new一个对象可能会出现指令重排的现象。这时
候,我们需要给声明的单例对象加上volatile关键字,保证可见性。
无疑会拖慢速度,使用双重加锁机制正好可以解决这个问题:

public class SLHanDanli {
   private static volatile SLHanDanli dl = null;
   private SLHanDanli(){}
     public static SLHanDanli getInstance(){
         if(dl == null){
           synchronized (SLHanDanli.class) {
                 if(dl == null){
                      dl = new SLHanDanli();
                 }
             }
         }
         return dl;
     }
 }

看了上面的代码,有没有感觉很无语,双重加锁难道不是需要两个synchronized进行加锁的吗?
  …

其实不然,这里的双重指的的双重判断,而加锁单指那个synchronized,为什么要进行双重判断,其实很简单,第一重判断,如果单例已经存在,
那么就不再需要进行同步操作,而是直接返回这个实例,如果没有创建,才会进入同步块,同步块的目的与之前相同,目的是为了防止有两个调用同时进行时,
导致生成多个实例,有了同步块,每次只能有一个线程调用能访问同步块内容,当第一个抢到锁的调用获取了实例之后,这个实例就会被创建,之后的所有调用
都不会进入同步块,直接在第一重判断就返回了单例。至于第二个判断,个人感觉有点查遗补漏的意味在内(期待高人高见)。
  补充:关于锁内部的第二重空判断的作用,当多个线程一起到达锁位置时,进行锁竞争,其中一个线程获取锁,如果是第一次进入则dl为null,会进行单例对象的创建,完成后释放锁,其他线程获取锁后就会被空判断拦截,直接返回已创建的单例对象。
  不论如何,使用了双重加锁机制后,程序的执行速度有了显著提升,不必每次都同步加锁。
  其实我最在意的是volatile的使用,volatile关键字的含义是:被其所修饰的变量的值不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存来实现,
从而确保多个线程能正确的处理该变量。该关键字可能会屏蔽掉虚拟机中的一些代码优化,所以其运行效率可能不是很高,所以,一般情况下,并不建议使用双重
加锁机制,酌情使用才是正理!
  更进一步说,其实使用volatile的目的是为了防止暴露一个未初始化的不完整单例实例,导致系统崩溃。因为创建单例实例其实需要经过以下几步:
首先分配内存空间、然后将内存空间的首地址指向引用(指针),最后调用构造器创建实例,由于在第二步的时候这个引用(指针)就会变的非null,
那么在第三步未执行,真正的单例实例还未创建完成的时候,一个线程过来在第一个校验中为false,将会直接将不完整的实例返回,从而造成系统崩溃。

类级内部类方式

饿汉式会占用较多的空间,因为其在类加载时就会完成实例化,而懒汉式又存在执行速率慢的情况,双重加锁机制呢?又有执行效率差的毛病,
有没有一种完美的方式可以规避这些毛病呢?
  貌似有的,就是使用类级内部类结合多线程默认同步锁,同时实现延迟加载和线程安全。

public class ClassInnerClassDanli {
     public static class DanliHolder{
         private static ClassInnerClassDanli dl = new ClassInnerClassDanli();
     }
     private ClassInnerClassDanli(){}
     public static ClassInnerClassDanli getInstance(){
         return DanliHolder.dl;
    }
 }

如上代码,所谓类级内部类,就是静态内部类,这种内部类与其外部类之间并没有从属关系,加载外部类的时候,并不会同时加载其静态内部类,
只有在发生调用的时候才会进行加载,加载的时候就会创建单例实例并返回,有效实现了懒加载(延迟加载),至于同步问题,我们采用和饿汉式
同样的静态初始化器的方式,借助JVM来实现线程安全。
  其实使用静态初始化器的方式会在类加载时创建类的实例,但是我们将实例的创建显式放置在静态内部类中,它会导致在外部类加载时不进行实例创建,
这样就能实现我们的双重目的:延迟加载和线程安全。

单例模式适用场景

好多没怎么使用过的人可能会想,单例模式感觉不怎么用到,实际的应用场景有哪些呢?以下,我将列出一些就在咱们周边和很有意义的单例应用场景。

  1. Windows的Task Manager(任务管理器)就是很典型的单例模式(这个很熟悉吧),想想看,是不是呢,你能打开两个windows task manager吗? 不信你自己试试看哦~

  2. windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。

  3. 网站的计数器,一般也是采用单例模式实现,否则难以同步。

  4. 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。

  5. Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。

  6. 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库
    连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因为何用单例模式来维护,就可以大大降低这种损耗。

  7. 多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。

  8. 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。

  9. HttpApplication 也是单位例的典型应用。熟悉ASP.Net(IIS)的整个请求生命周期的人应该知道HttpApplication也是单例模式,所有的HttpModule都共享
    一个HttpApplication实例.

    1.需要生成唯一序列的环境
    2.需要频繁实例化然后销毁的对象。
    3.创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
    4.方便资源相互通信的环境
    优点:1.实现了对唯一实例访问的可控
    2.对于一些需要频繁创建和销毁的对象来说可以提高系统的性能。
    缺点:1. 不适用于变化频繁的对象
    2.滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出。
    3.如果实例化的对象长时间不被利用,系统会认为该对象是垃圾而被回收,这可能会导致对象状态的丢失。

Spring 的单例实现原理

在Spring中,Bean默认是单例的,这是因为Spring容器在初始化时会将Bean对象创建并缓存在容器中,默认情况下,Spring容器使用单例模式来管理Bean实例。
以下是 Spring 实现单例的主要步骤和原理:

  1. Bean 定义:在 Spring 的配置文件中(如 XML 文件)或通过注解方式,我们定义 Bean 及其相关属性。其中,scope 属性用于指定 Bean 的作用域,默认值是 “singleton”,表示该 Bean 是一个单例。
  2. 容器初始化:当 Spring 容器启动时,它会读取配置文件或注解信息,并解析 Bean 的定义。对于每个单例 Bean,Spring 容器会创建一个实例,并将其存储在容器的内部缓存中。
  3. Bean 获取:当应用中的代码通过 ApplicationContext 或 BeanFactory 调用 getBean() 方法来获取某个单例 Bean 时,Spring 容器会首先检查其内部缓存。如果该 Bean 已经存在(即已经被创建过),则直接返回该实例;否则,根据 Bean 的定义创建一个新的实例,并存储在缓存中,然后返回该实例。
  4. 单例保证:由于 Spring 容器在内部缓存了单例 Bean 的实例,并且在每次获取 Bean 时都优先从缓存中查找,因此确保了在整个应用上下文中,对于同一个单例 Bean 的定义,始终返回同一个实例。
  5. 容器销毁:当 Spring 容器被销毁时,它会负责销毁所有它管理的 Bean,包括单例 Bean。这确保了资源的正确释放和避免内存泄漏。
    需要注意的是,虽然 Spring 保证了单例 Bean 在容器范围内的唯一性,但并不意味着它在整个 JVM 中都是唯一的。如果有多个 Spring 容器(例如,每个 Web 应用都有一个自己的 Spring 容器),那么每个容器都会创建自己的单例 Bean 实例。此外,对于原型(prototype)作用域的 Bean,Spring 每次都会创建一个新的实例,而不是共享同一个实例。
    Spring的单例实现原理主要基于两个方面:
  6. 默认作用域: 在Spring中,默认情况下,Bean的作用域(Scope)是单例(Singleton)。这意味着Spring容器中的每个Bean定义都只会创建一个实例,并在需要时重复使用这个实例。
  7. 容器管理: Spring容器是一个大型的对象管理容器,它负责创建、装配和管理Bean对象。当配置文件或注解启动Spring容器时,容器会按照配置创建Bean的实例并管理它们的生命周期。容器会在启动时实例化所有的单例Bean,并在需要时返回它们的引用,以确保单例的唯一性。
    需要注意的是,虽然Spring默认将Bean配置为单例模式,但也可以通过在Bean的定义中显式地指定其他作用域(如原型、请求、会话等)来改变Bean的作用域。例如,在XML配置文件中可以使用元素的scope属性,或者在使用注解配置时可以使用@Scope注解来指定Bean的作用域。
    总的来说,Spring的单例实现原理是基于容器管理和作用域定义的,它确保在Spring容器中每个Bean的实例是唯一的,并且可以在需要时被共享和重用。
    Spring 框架中的单例实现原理主要依赖于其 IoC(控制反转)容器。在 Spring 中,当我们定义一个 Bean 时,Spring 容器会负责创建和管理这个 Bean 的生命周期。对于单例模式的 Bean,Spring 容器会确保只创建一个实例,并在整个应用上下文中共享这个实例。

单例被破坏的五个场景

分别为多线程破坏单例、指令重排破坏单例、克隆破坏单例、反序列化破坏单例、反射破坏单例。
1.多线程破坏单例
在多线程环境下,线程的时间片是由CPU自由分配的,具有随机性,而单例对象作为共享资源可能会
同时被多个线程同时操作,从而导致同时创建多个对象。当然,这种情况只出现在懒汉式单例中。如果是
饿汉式单例,在线程启动前就被初始化了,不存在线程再创建对象的情况。
如果懒汉式单例出现多线程破坏的情况,我给出以下两种解决方案:
1、改为DCL双重检查锁的写法。
2、使用静态内部类的写法,性能更高。

2.指令重排
指令重排也可能导致懒汉式单例被破坏。来看这样一句代码:
instance = new Singleton();
看似简单的一段赋值语句:instance = new Singleton();
其实JVM内部已经被转换为多条执行指令:
memory = allocate(); 分配对象的内存空间指令
ctorInstance(memory); 初始化对象
instance = memory; 将已分配存地址赋值给对象引用
1、分配对象的内存空间指令,调用allocate()方法分配内存。
2、调用ctorInstance()方法初始化对象
3、将已分配存地址赋值给对象引用
但是经过重排序后,执行顺序可能是这样的:
memory = allocate(); 分配对象的内存空间指令
instance = memory; 将已分配存地址赋值给对象引用
ctorInstance(memory); 初始化对象
1、分配对象的内存空间指令
2、设置instance指向刚分配的内存地址
3、初始化对象
我们可以看到指令重排之后,instance指向分配好的内存放在了前面,而这段内存的初始化的指令被
排在了后面,在线程 T1 初始化完成这段内存之前,线程T2 虽然进不去同步代码块,但是在同步代码块之
前的判断就会发现 instance 不为空,此时线程T2 获得 instance 对象,如果直接使用就可能发生错误。
如果出现这种情况,我该如何解决呢?只需要在成员变量前加volatile,保证所有线程的可见性就可
以了。private static volatile Singleton instance = null;

3.克隆破坏单例
在Java中,所有的类就继承自Object,也就是说所有的类都实现了clone()方法。如果是深clone(),
每次都会重新创建新的实例。那如果我们定义的是单例对象,岂不是也可调用clone()方法来反复创建新的
实例呢?确实,这种情况是有可能发生的。为了避免发生这样结果,我们可以在单例对象中重写clone()
方法,将单例自身的引用作为返回值。这样,就能避免这种情况发生。

4.反序列化破坏单例
我们将Java对象序列化以后,对象通常会被持久化到磁盘或者数据库。如果我们要再次加载到内存,
就需要将持久化的内容反序列化成Java对象。反序列化是基于字节码来操作的,我们要序列化以前的内容
进行反序列化到内存,就需要重新分配内存,也就是说,要重新创建对象。那如果要反序列化的对象恰恰
是单例对象,我们该怎么办呢?
我告诉大家一种解决方案,在反序列的过程中,Java API会调用readResolve()方法,可以通过获取
readResolve()方法的返回值覆盖反序列化创建的对象。
因此,只需要重写readResolve()方法,将返回值设置为已经存在的单例对象,就可以保证反序列化
以后的对象是同一个了。之后再将反序列化后的对象中的值,克隆到单例对象中。

5.反射破坏单例
以上讲的所有单例情况都有可能被反射破坏。因为Java中的反射机制是可以拿到对象的私有的构造方
法,也就是说,反射可以任意调用私有构造方法创建单例对象。当然,没有人会故意这样做,但是如果出
现意外的情况,该如何处理呢?我推荐大家两种解决方案,
第一种方案是在所有的构造方法中第一行代码进行判断,检查单例对象是否已经被创建,如果已经被
创建,则抛出异常。这样,构造方法将会被终止调用,也就无法创建新的实例。
第二种方案,将单例的实现方式改为枚举式单例,因为在JDK源码层面规定了,不允许反射访问枚举。

单例的实现方式

饿汉式单例
懒汉式-延迟加载方式
懒汉式双重加锁机制
静态内部类
容器式单例
注册式-枚举单例
ThreadLocal 线程内部
饿汉式单例
优点:执行效率高,性能高,线程安全
缺点:类加载时用不用都会初始化,资源浪费

package com.lc.singleton;

/**
 * @Author lc
 * @description:
 * 1.  * 饿汉式单例
 * 2.  * 优点:执行效率高,性能高,线程安全
 * 3.  * 缺点:类加载时用不用都会初始化,资源浪费
 * @Date 2023/4/1 18:25
 */
public class HungrySingleton {

    private HungrySingleton() {
    };

    private static HungrySingleton hungrySingleton = new HungrySingleton();

    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }

    

    public static void main(String[] args) {
        for (int i = 0; i <20 ; i++) {
            new Thread(()->{
                System.out.println(HungrySingleton.getInstance().hashCode());
            }).start();
        }
    }
}

懒汉式-延迟加载方式

/**
 *
 * @Author lc
 * @description:懒汉式-延迟加载方式
 * @Date 2023/4/1 16:26
 */
public class SingletonLazy {
    private SingletonLazy(){};
    private static SingletonLazy singletonLazy ;
    public static SingletonLazy getInstance(){
        if(singletonLazy==null){
            singletonLazy=new SingletonLazy();
        }
        return singletonLazy;
    }

}

懒汉式双重加锁机制

package com.lc.singleton.lazy;

/**
 * @Author lc
 * @description: 懒汉式双重加锁机制
 * 有使用synchronized关键字来同步获取实例,保证单例的唯一性,使用双重加锁机制正
 *
 * 懒汉式-双重检查锁
 * 优点:被外部调用的时候创建对象,节省资源,性能高,线程安全
 * 缺点:可读性难度加大,代码不够优雅

 * @Date 2023/4/1 16:26
 */
public class SingletonLazyDoubleCheck {
    private SingletonLazyDoubleCheck() {};
    //volatile关键字的含义是:被其所修饰的变量的值不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存来实现,
    // 从而确保多个线程能正确的处理该变量,volatile的目的是为了防止暴露一个未初始化的不完整单例实例,导致系统崩溃
    private volatile static SingletonLazyDoubleCheck singletonLazy;
   //volatile来禁止指令重排
    public static SingletonLazyDoubleCheck getInstance() {
        //检查是否阻塞,如果已经创建过,就不需要再进入加锁代码块拉
        //低性能,大大的提升性能,如果没有该检查,每次都会去竞争锁
        if (singletonLazy == null) {
            synchronized (SingletonLazyDoubleCheck.class) {
                //检查是否重新创建实例
                if (singletonLazy == null) {
                    singletonLazy = new SingletonLazyDoubleCheck();
                }
            }
        }
        return singletonLazy;
    }
}

静态内部类

package com.lc.singleton.lazy;

/**
 * @Author lc
 * @description: 静态内部类
 * 懒汉式-静态内部类
 *  * 优点:性能高,节省资源,利用了java本身的语法特点,不能够被反射破坏
 *  * 缺点:代码不优雅
 * @Date 2023/4/1 20:39
 */
public class SingletonLazyStaticInnerClass {
  private  SingletonLazyStaticInnerClass(){};
    private static SingletonLazyStaticInnerClass getInstance(){
      return lazyHolder.STATIC_INNER_CLASS;
    }
  private  static  class lazyHolder{
        private  static  final  SingletonLazyStaticInnerClass STATIC_INNER_CLASS=new SingletonLazyStaticInnerClass();
  }
}

容器式单例

package com.lc.singleton.register;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @Author lc
 * @description: 容器式单例  目前线程不安全 ,解决方案 加锁
 * 可参考spring--AbstractFactoryBean  getBean(String name)
 * @Date 2023/4/1 20:47
 */
public class ContainerSingleton {
    private ContainerSingleton() {};
    private static Map<String, Object> ioc = new ConcurrentHashMap<>();
    public static Object getInstance(String className) {
        Object instance = null;
     if (!ioc.containsKey(className)){
         try {
             instance = Class.forName(className).newInstance();
             ioc.put(className,instance);
         } catch (Exception e) {
             throw new RuntimeException(e);

         }
     }else{
         instance= ioc.get(className);
     }

     return instance;
    }

    public static void main(String[] args) {
        Object instance1 = ContainerSingleton.getInstance("com.lc.singleton.register.User");
        Object instance2 = ContainerSingleton.getInstance("com.lc.singleton.register.User");
        System.out.println(instance1==instance2);
    }
}

注册式-枚举单例

package com.lc.singleton.register;

/**
 * @Author lc
 * @description:注册式-枚举
 * * 枚举式单例
 *  * 优点:线程安全,不能被反射破坏
 *  * 缺点:不适用大批量单例对象,浪费资源
 * @Date 2023/4/1 20:24
 */
public enum EnunSingleton {
    INSTANCE;
    private Object data;

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }
    public static EnunSingleton getInstance() {
        return INSTANCE;
    }

    public static void main(String[] args) {
        EnunSingleton.getInstance().setData("你好");
    }
}

ThreadLocal 线程内部

package com.lc.singleton;

/**
 * @Author lc
 * @description:  ThreadLocal 线程内部
 * @Date 2023/4/1 21:35
 */
public class ThreadLocalSingleton {

    private static final ThreadLocal<ThreadLocalSingleton> THREAD_LOCAL_SINGLETON_THREAD_LOCAL =
            new ThreadLocal<ThreadLocalSingleton>() {
                @Override
                protected ThreadLocalSingleton initialValue() {
                    return new ThreadLocalSingleton();
                }
            };

    private ThreadLocalSingleton() {};

    public static ThreadLocalSingleton getInstance() {
        return THREAD_LOCAL_SINGLETON_THREAD_LOCAL.get();
    }

    public static void main(String[] args) {
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());



    }
}

实现线程安全的单例模式

● 在多线程环境中,不要使用简单的懒加载方式(只在getInstance()方法内部使用synchronized),因为这种方式在每次调用getInstance()时都会进行同步,性能较差。
● 使用双重检查锁定或静态内部类方式时,要注意构造函数不要有复杂的逻辑,以避免指令重排导致的问题。虽然使用volatile可以解决这个问题,但最好保持构造函数的简单性。
● 如果单例需要被序列化,需要增加防止反序列化的机制,例如实现readResolve()方法。
通常推荐使用枚举或静态内部类的方式来实现线程安全的单例,因为它们既简单又安全。

实现线程安全的单例模式,有多种方式可以确保在并发环境下单例的唯一性。以下是一些常见的方法:

1. 饿汉式(静态初始化)

public class Singleton {  
    private static final Singleton INSTANCE = new Singleton();  
  
    private Singleton() {  
        // 私有构造方法,防止外部通过 new Singleton() 创建实例  
    }  
  
    public static Singleton getInstance() {  
        return INSTANCE;  
    }  
}

这种方式在类加载时就完成了初始化,所以天生就是线程安全的。
2. 懒汉式(双重检查锁定)

public class Singleton {  
    private volatile static Singleton instance;  
  
    private Singleton() {  
        // 私有构造方法,防止外部通过 new Singleton() 创建实例  
    }  
  
    public static Singleton getInstance() {  
        if (instance == null) { // 第一次检查实例是否存在,如果不存在才进入下面的同步块  
            synchronized (Singleton.class) {  
                if (instance == null) { // 第二次检查实例是否存在,如果不存在才创建实例  
                    instance = new Singleton();  
                }  
            }  
        }  
        return instance;  
    }  
}

双重检查锁定是一种延迟加载技术,避免了饿汉式在类加载时就完成初始化的开销。同时,由于使用了volatile关键字和双重检查,保证了线程安全。
3. 静态内部类

public class Singleton {  
    private Singleton() {  
        // 私有构造方法,防止外部通过 new Singleton() 创建实例  
    }  
  
    private static class SingletonHolder {  
        private static final Singleton INSTANCE = new Singleton();  
    }  
  
    public static Singleton getInstance() {  
        return SingletonHolder.INSTANCE;  
    }  
}

静态内部类的方式利用了 JVM 的类加载机制来保证线程安全。当SingletonHolder类被加载时,会初始化其静态变量INSTANCE,由于 JVM 在类加载时是线程安全的,因此这种方式也是线程安全的。
4. 枚举(推荐)

public enum Singleton {  
    INSTANCE;  
  
    public void whateverMethod() {  
        // 方法体  
    }  
}

在 Java 中,枚举是线程安全的,并且只会加载一次。因此,使用枚举来实现单例是最简单且最安全的方式。
5. 使用 java.util.concurrent.atomic.AtomicReference

import java.util.concurrent.atomic.AtomicReference;  
  
public class Singleton {  
    private static final AtomicReference<Singleton> INSTANCE_REF = new AtomicReference<>();  
  
    private Singleton() {  
        // 私有构造方法,防止外部通过 new Singleton() 创建实例  
    }  
  
    public static Singleton getInstance() {  
        for (;;) {  
            Singleton current = INSTANCE_REF.get();  
            if (current != null) {  
                return current;  
            }  
            Singleton newInstance = new Singleton();  
            if (INSTANCE_REF.compareAndSet(null, newInstance)) {  
                return newInstance;  
            }  
            // 如果当前实例已经被其他线程初始化,则丢弃新创建的实例,并重试  
        }  
    }  
}

使用AtomicReference和CAS(Compare-and-Swap)操作可以确保线程安全地实现单例。这种方式比双重检查锁定更为复杂,但在高并发场景下可能具有更好的性能。