单例模式(设计模式)

概述

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

  1. 控制实例数量:确保全局范围内只有一个实例存在,节省系统资源。
  2. 全局访问:提供一个全局访问点,可以在任何地方访问该实例。
  3. 避免重复初始化:通过控制实例化过程,避免多次创建对象所带来的开销。
    单例模式的缺点
  4. 并发问题:在多线程环境下实现不当可能导致并发问题,需要额外处理线程安全。
  5. 测试困难:单例模式可能导致代码耦合度增加,难以进行单元测试。
  6. 隐藏依赖关系:使用单例模式可能隐藏类之间的依赖关系,降低代码可读性。
    单例模式广泛应用于需要唯一实例的场景,如数据库连接池、日志管理器、配置管理器等。合理使用单例模式可以有效控制资源,提升系统性能和稳定性。
    饿汉式单例
    懒汉式-延迟加载方式
    懒汉式双重加锁机制
    静态内部类
    容器式单例
    枚举单例
    ThreadLocal 线程内部
    单例模式的关键特性
  7. 唯一性:确保一个类只有一个实例。
  8. 全局访问点:提供一个全局访问点来获取该实例。

1. 饿汉式(hungry Initialization)

又何为饿?饿者,饥不择食;但凡有食,必急食之。此处同义:在加载类的时候就会创建类的单例,并保存在类中。
饿汉式单例在类加载时就完成了实例化。这种方式天生就是线程安全的,但在类加载时即完成实例创建,如果这个实例没有被使用,则会造成资源浪费。
实现线程安全的单例模式,有多种方式可以确保在并发环境下单例的唯一性。
这种方式在类加载时就完成了初始化,所以天生就是线程安全的。
优点:执行效率高,性能高,线程安全,实现简单,无需考虑线程安全问题。
缺点:即使没有使用到单例实例,也会在类加载时创建,可能浪费资源。
饿汉式(静态初始化)

public class Singleton {  
     // 在类加载时创建实例
    //此处定义类变量实例并直接实例化,在类加载的时候就完成了实例化并保存在类中
    private static final Singleton INSTANCE = new Singleton();  
    //定义无参构造器,用于单例实例,   // 私有构造函数,防止外部实例化
    private Singleton() {  
        // 私有构造方法,防止外部通过 new Singleton() 创建实例  
    }  
    //定义公开方法,返回已创建的单例// 提供全局访问点
    public static Singleton getInstance() {  
        return INSTANCE;  
    }  
}
package com.lc.singleton;

/**
 * @Author lc
 * @description:饿汉式单例
 * @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();
        }
    }
}

2. 懒汉式(Lazy Initialization)

何为懒?顾名思义,就是不做事,这里也是同义,懒汉式就是不在系统加载时就创建类的单例,而是在第一次使用实例的时候再创建。
懒汉式在需要时才创建实例,延迟实例化。
优点:只有在需要时才创建实例,节省资源。
缺点:需要加锁,性能开销较大。

懒汉式单例(同步方法,线程安全但性能较差)

public class Singleton {
    // 静态变量保存实例//定义一个私有类变量来存放单例,私有的目的是指外部无法直接获取这个变量,而要使用提供的公共方法来获取
    private static Singleton instance;

    // 私有构造函数,防止外部实例化
     //定义私有构造器,表示只在类内部使用,亦指单例的实例只能在单例类内部创建
    private Singleton() {}

    // 提供全局访问点,并确保线程安全
//定义一个公共的公开的方法来返回该类的实例,由于是懒汉式,需要在第一次使用时生成实例,所以为了线程安全,
    //使用synchronized关键字来确保只会生成单例
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

懒汉式单例(线程不安全版)
懒汉式单例在第一次需要使用时才进行实例化,但这种方式在多线程环境下是不安全的。


/**
*

  • @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;
    }

}


3.双重检查锁定(Double-Checked Locking)

这种方法结合了懒汉式的延迟加载和饿汉式的线程安全,优化了性能。
双重检查锁机制既保证了线程安全,又减少了同步开销。

public class Singleton {
    // 使用 volatile 修饰防止指令重排序
    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;
    }
}

懒汉式双重加锁机制

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;
    }
}

优点:在第一次调用时初始化,且只加锁一次,性能较高。
缺点:实现相对复杂,可能出现潜在的代码维护问题。
  在懒汉式实现单例模式的代码中,有使用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,将会直接将不完整的实例返回,从而造成系统崩溃。

4. 静态内部类(Static Inner Class)

利用类加载机制实现懒加载,同时又是线程安全的。
public class Singleton {
// 私有构造函数,防止外部实例化
private Singleton() {}

// 静态内部类,只有在调用 getInstance() 时才会加载
private static class SingletonHelper {
    private static final Singleton INSTANCE = new Singleton();
}
// 提供全局访问点
public static Singleton getInstance() {
    return SingletonHelper.INSTANCE;
}

}
静态内部类的方式利用了 JVM 的类加载机制来保证线程安全。当SingletonHolder类被加载时,会初始化其静态变量INSTANCE,由于 JVM 在类加载时是线程安全的,因此这种方式也是线程安全的。
优点:利用 JVM 类加载机制保证线程安全,且实现了懒加载。
缺点:实现相对简单,但依赖于静态内部类特性。
静态内部类

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();
  }
}

饿汉式会占用较多的空间,因为其在类加载时就会完成实例化,而懒汉式又存在执行速率慢的情况,双重加锁机制呢?又有执行效率差的毛病,
有没有一种完美的方式可以规避这些毛病呢?
  貌似有的,就是使用类级内部类结合多线程默认同步锁,同时实现延迟加载和线程安全。
  如上代码,所谓类级内部类,就是静态内部类,这种内部类与其外部类之间并没有从属关系,加载外部类的时候,并不会同时加载其静态内部类,只有在发生调用的时候才会进行加载,加载的时候就会创建单例实例并返回,有效实现了懒加载(延迟加载),至于同步问题,我们采用和饿汉式.同样的静态初始化器的方式,借助JVM来实现线程安全。其实使用静态初始化器的方式会在类加载时创建类的实例,但是我们将实例的创建显式放置在静态内部类中,它会导致在外部类加载时不进行实例创建,这样就能实现我们的双重目的:延迟加载和线程安全。

5. 枚举(Enum)

使用枚举类型实现单例是最简单和有效的方法之一,保证了序列化机制的单例性。
在 Java 中,枚举是线程安全的,并且只会加载一次。因此,使用枚举来实现单例是最简单且最安全的方式。

public enum Singleton {
    INSTANCE;

    public void someMethod() {
        // 方法实现
    }
}

优点:实现简单,天然支持序列化机制,绝对防止多次实例化。
缺点:不能延迟加载实例,如果需要懒加载则不适用。

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("你好");
    }
}

6.容器式单例

容器式单例是一种管理和存储单例对象的方式,通常用于大型应用程序中。它利用一个容器(如 Map 或者其他集合)来存储不同的单例实例,以便在需要时能够快速、方便地获取这些实例。这样做的好处是可以更灵活地管理单例对象,并且支持多种类型的单例。
下面是一个使用 ConcurrentHashMap 实现容器式单例的示例:

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

public class SingletonContainer {

    private static final ConcurrentMap<String, Object> instances = new ConcurrentHashMap<>();

    private SingletonContainer() {
        // 私有构造函数防止外部实例化
    }

    public static <T> T getInstance(String key, Class<T> clazz) throws IllegalAccessException, InstantiationException {
        if (!instances.containsKey(key)) {
            synchronized (SingletonContainer.class) {
                if (!instances.containsKey(key)) {
                    instances.put(key, clazz.newInstance());
                }
            }
        }
        return clazz.cast(instances.get(key));
    }
}
1. 定义单例类,例如:
public class MySingleton {
    private MySingleton() {
        // 私有构造函数
    }

    public void doSomething() {
        System.out.println("Doing something...");
    }
}
2. 在需要的时候从容器中获取该单例实例:
public class Main {
    public static void main(String[] args) {
        try {
            MySingleton instance = SingletonContainer.getInstance("MySingleton", MySingleton.class);
            instance.doSomething();
        } catch (InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

优点
● 灵活性:可以管理多种不同类型的单例。
● 线程安全:使用 ConcurrentHashMap 和双重检查锁定确保线程安全。
● 延迟初始化:只有在第一次请求时才会创建实例。
缺点
● 复杂性增加:相比传统单例模式,容器式单例的实现和管理稍微复杂。
● 性能开销:每次获取实例时都需要进行同步检查,可能会带来一定的性能开销。
改进
如果不想每次都需要传递字符串键和类对象,可以考虑进一步封装,使其更加简洁易用。例如,可以通过注解或者配置文件的方式来自动注册单例类,或者使用工厂模式来隐藏具体的实现细节。这些改进可以根据实际需求进行选择和调整。

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);
    }
}

7.ThreadLocal

ThreadLocal 是 Java 中的一个线程局部变量,它为每个线程提供了独立的变量副本。通过 ThreadLocal 可以在每个线程中保存一个单独的实例,从而实现线程内部的单例模式。
以下是使用 ThreadLocal 实现线程内部单例模式的示例:
public class Singleton {

// 使用 ThreadLocal 来管理单例实例
private static final ThreadLocal<Singleton> threadLocalInstance = new ThreadLocal<Singleton>() {
    @Override
    protected Singleton initialValue() {
        return new Singleton();
    }
};

// 私有构造函数防止外部实例化
private Singleton() {
    // 初始化逻辑
}

public static Singleton getInstance() {
    return threadLocalInstance.get();
}

// 示例方法
public void doSomething() {
    System.out.println("Doing something...");
}

public static void main(String[] args) {
    Singleton singleton = Singleton.getInstance();
    singleton.doSomething();
}

}

  1. ThreadLocal 管理单例实例:
    private static final ThreadLocal threadLocalInstance = new ThreadLocal() {…}:创建一个 ThreadLocal 对象,并覆盖其 initialValue() 方法来初始化单例实例。
  2. 私有构造方法:
    private Singleton() {}:防止外部类通过构造方法实例化该类。
  3. 获取单例实例的方法:
    public static Singleton getInstance() {}:定义一个静态方法来获取单例实例。
    return threadLocalInstance.get():使用 ThreadLocal 的 get() 方法获取当前线程对应的单例实例。如果当前线程还没有对应的实例,则会调用 initialValue() 方法进行初始化,并将实例与当前线程关联。
    优点
    线程隔离:每个线程都拥有自己的单例实例副本,不同线程之间相互独立,避免了线程安全问题。
    延迟加载:实例只有在需要时才会被创建,实现了按需加载。
    缺点
    资源占用:对于每个线程都会创建一个实例副本,如果线程数量过多或单例实例较大,会占用更多的内存空间。
    无法跨线程共享:每个线程都有自己的实例副本,无法实现跨线程共享。
    使用 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());
    }
}

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

单例模式应用

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

  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.如果实例化的对象长时间不被利用,系统会认为该对象是垃圾而被回收,这可能会导致对象状态的丢失。

单例被破坏的五个场景

分别为多线程破坏单例、指令重排破坏单例、克隆破坏单例、反序列化破坏单例、反射破坏单例。
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源码层面规定了,不允许反射访问枚举。

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. 设计模式

    2024-06-15 12:00:03       34 阅读
  2. 设计模式

    2024-06-15 12:00:03       16 阅读
  3. 设计模式

    2024-06-15 12:00:03       13 阅读
  4. 设计模式

    2024-06-15 12:00:03       12 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-06-15 12:00:03       14 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-06-15 12:00:03       16 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-06-15 12:00:03       15 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-06-15 12:00:03       18 阅读

热门阅读

  1. 【docker】如何修改已有容器的端口映射

    2024-06-15 12:00:03       6 阅读
  2. springMVC入门案例

    2024-06-15 12:00:03       6 阅读
  3. Node.js环境安装与管理指南

    2024-06-15 12:00:03       10 阅读
  4. 圆锥曲线的分类

    2024-06-15 12:00:03       8 阅读
  5. 深度解析服务发布策略之蓝绿发布

    2024-06-15 12:00:03       7 阅读
  6. 缓存缓存缓存

    2024-06-15 12:00:03       9 阅读
  7. Sklearn基础教程

    2024-06-15 12:00:03       8 阅读
  8. 网络安全突发事件应急预案

    2024-06-15 12:00:03       9 阅读
  9. 智能合约中权限管理不当

    2024-06-15 12:00:03       7 阅读