聊聊设计模式——单例模式

目录

单例模式的5种实现方式:

1.饿汉式(Eager Initialization):

2.懒汉式(Lazy Initialization):

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

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

5.注册式单例(Registration Style Singleton):

三种破坏单例模式的方式:

1.反射

2.序列化和反序列化 

3.原型模式调用clone()方法

使用场景:

涉及单例模式的JDK源码:


单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

优点

  1. 全局唯一性:单例模式确保在应用程序中只有一个实例,这对于共享资源、配置管理和维护全局状态非常有用。

  2. 懒加载:如果实例在第一次使用时才被创建,可以延迟实例化,从而节省资源。

  3. 全局访问点:单例提供了一个统一的入口点,使得可以轻松地访问单例实例,而不需要传递实例的引用。

  4. 节省内存:由于只有一个实例存在,因此节省了多个相同对象的内存。

  5. 避免竞态条件:在多线程环境中,单例模式可以通过加锁等机制来确保只有一个实例被创建,从而避免竞态条件。

缺点

  1. 全局状态:单例模式引入了全局状态,这可能导致代码的复杂性和耦合性增加。全局状态可能会导致难以调试和理解的问题。

  2. 不适用于多线程:在多线程环境中,如果没有正确处理,单例模式可能导致性能问题或竞态条件。需要额外的措施来确保线程安全。

  3. 隐藏依赖:使用单例模式的类隐藏了其依赖关系,因为它们直接访问全局实例。这可能会使代码更难测试和维护。

  4. 不支持子类化:有时候,单例模式难以支持子类化,因为它将构造函数私有化,防止直接实例化子类。

  5. 单一职责原则:有时,将全局状态与其他功能耦合在一起可能会违反单一职责原则。

总之,单例模式是一个有用的设计模式,可以在需要确保只有一个实例存在的情况下使用。然而,开发人员应该谨慎使用,考虑到它可能引入的全局状态和复杂性。在多线程环境中,需要特别小心,以确保线程安全。

单例模式的5种实现方式:

1.饿汉式(Eager Initialization)
  • 优点

    • 线程安全:实例在类加载时就创建,不会出现多线程竞争的问题。
    • 简单:实现简单,不需要额外的同步措施。
  • 缺点

    • 资源浪费:如果实例在应用程序启动时创建,可能会导致资源浪费。
    • 不支持延迟初始化:无法实现延迟初始化,不适用于大型对象
public class HungrySingleton {
    // 私有化构造方法:阻止通过构造方法实例化单例对象
    private HungrySingleton(){}
    // 私有属性:单例对象
    private static HungrySingleton hungrySingleton = new HungrySingleton();
    // 提供全局访问点 
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}

 静态代码块的方式也属于类加载时候执行

public class StaticHungrySingleton {
    private StaticHungrySingleton(){}
    private static StaticHungrySingleton hungrySingleton;
    static {
        hungrySingleton = new StaticHungrySingleton();
    }
    public static StaticHungrySingleton getInstance(){
        return hungrySingleton;
    }
}
2.懒汉式(Lazy Initialization)
  • 优点

    • 延迟初始化:实例只在第一次使用时创建,节省了资源。
    • 线程安全:可以通过加锁等机制实现线程安全。
  • 缺点

    • 线程竞争:在多线程环境中,可能出现竞态条件,需要额外的同步措施。
    • 性能开销:因为需要在访问时检查实例是否已创建,可能会导致性能开销。

a.线程不安全的懒汉式:

public class LazySimpleSingleton {
    private static LazySimpleSingleton lazySingleton = null;
    private LazySimpleSingleton(){}
    public static LazySimpleSingleton getInstance(){
        if(lazySingleton == null){
            lazySingleton = new LazySimpleSingleton();
        }
        return lazySingleton;
    }
}

多线程环境下不安全的原因:如果多个线程能够同时进入if(lazySingleton == null),并且此时lazySingleton为空,就会有多个线程执行lazySingleton = new LazySimpleSingleton();将导致多次实例化lazySingleton.

b.线程安全的懒汉式:

通过对getInstance()方法加锁,可以实现一个时间点只有一个线程能够进入该方法,避免多次实例化的问题。

public class LazySynchronizedSingleton {
    private static LazySynchronizedSingleton lazySingleton = null;
    private LazySynchronizedSingleton(){}
    public static synchronized LazySynchronizedSingleton getInstance(){
        if(lazySingleton == null){
            lazySingleton = new LazySynchronizedSingleton();
        }
        return lazySingleton;
    }
}
3.双重检查锁定(Double-Checked Locking)
  • 优点

    • 延迟初始化:实现了延迟初始化,避免了资源浪费。
    • 线程安全:通过双重检查和同步块来确保线程安全。
  • 缺点

    • 复杂性:实现较为复杂,需要小心处理细节,否则可能会出现错误

方法加上synchronized锁解决了线程安全问题,但是在线程数量比较多的情况下,大量线程会阻塞在方法外部,导致程序性能下降。所以为了兼顾性能和线程安全问题,我们可以通过双重检查锁的方式创建懒汉式单例。

public class LazyDoubleCheckSingleton {
    private volatile static LazyDoubleCheckSingleton lazySingleton = null;// volatile禁止指令重排序
    private LazyDoubleCheckSingleton(){}
    public static synchronized LazyDoubleCheckSingleton getInstance(){
        if(lazySingleton == null){ // #1
            // 如果lazySingleton为空才加锁进行初始化
            synchronized(LazyDoubleCheckSingleton.class){ // #2
                if (lazySingleton == null){ // #3
                    /* 如果没有第二次判断就会发生:
                    1.A执行完#1
                    2.在#1和#2之间发生了线程切换,切换到了B
                    3.线程B执行了#2,获取到了锁,并初始化了lazySingleton
                    4.切换到了线程A,线程A执行了#2,获取到了锁,并初始化了lazySingleton
                    这样就导致初始化了两次,所以必须进行二次校验
                    */
                    lazySingleton = new LazyDoubleCheckSingleton(); // #4
                }
            }
        }
        return lazySingleton;
    }
}

volatile关键字的作用是防止多线程环境下#3和#4发生重排序,可能导致NPE(空指针异常)。 

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

    • 懒加载:实现了延迟初始化,且不需要额外的同步措施。
    • 线程安全:利用类加载机制保证了线程安全性。
  • 缺点

    • 不支持传递参数:无法传递参数给单例的构造函数。

我们知道用到synchronized关键字总归要上锁,对程序性能还是存在一定影响的。从类的初始化角度来考虑,可以采用静态内部类的方式。

/*
懒汉式单例-静态内部类只会在调用instance的时候初始化,延迟初始化的同时虚拟机提供了对线程安全的支持。
 */
public class LazyStaticInnerSingleton {
    private LazyStaticInnerSingleton(){}
    public static synchronized LazyStaticInnerSingleton getInstance(){
        return LazySingletonHolder.lazySingleton;
    }
    private static class LazySingletonHolder {
        private static LazyStaticInnerSingleton lazySingleton = new LazyStaticInnerSingleton();
    }
}
5.注册式单例(Registration Style Singleton):

a.枚举(Enum):

  • 优点

    • 简单:实现简单,线程安全,不需要处理细节问题。
    • 序列化安全:自带序列化机制,防止通过反序列化创建新实例。
  • 缺点

    • 不支持懒加载:枚举类型的实例在类加载时就创建,无法实现懒加载。
/*
Effective Java 推荐的方式:使用jvm封装的枚举类通过注册的形式获取单例,实际上也算一种饿汉式,但是性能损耗已经被jvm处理过,相比于个人进行优化要好得多。
 */
public enum LazyEnumSingleton {
    INSTANCE;
    // 使用时直接调用LazyEnumSingleton.INSTANCE.sayHello()实现单例。
    public void sayHello(){
        System.out.println("Hello world !");
    }
}

b.容器实现(Container Managed Singleton)

  • 优点

    • 灵活性:容器可以管理单例的生命周期,适用于某些特定的企业应用场景。
  • 缺点

    • 依赖容器:需要依赖容器(如Java EE容器)来管理单例,不适用于所有应用。
    • 复杂性:需要配置和维护容器。
public class ContainerSingleton {
    private ContainerSingleton(){}
    private static Map<String, Object> ioc = new ConcurrentHashMap<>();
    public static Object getBean(String className){
        synchronized (ioc){
            if (!ioc.containsKey(className)) {
                Object obj = null;
                try {
                    obj = Class.forName(className).newInstance();
                    ioc.put(className, obj);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return obj;
            } else {
                return ioc.get(className);
            }
        }
    }
}

c.本地线程单例(ThreadLocal):

  • 优点

    • 单个线程安全:能保证在单个线程中是唯一的,天生的线程安全。
  • 缺点

    • 不保证全局唯一:ThreadLocal 不能保证其创建的对象是全局唯一。
/*
ThreadLocal也算是一种注册式的单例实现:线程间隔离的ThreadLocal其实是存储在一个TreadLocalMap中,初始化的时候就put到了Map里,有就直接拿出来用。
ThreadLocal采用空间换时间的方式为每一个线程都提供了一份变量,同时访问而不影响。同步机制是采取时间换空间的方式排队访问。
 */
public class ThreadLocalSingleton {
    private ThreadLocalSingleton(){}
    private static final ThreadLocal<ThreadLocalSingleton> tlSingleton = new ThreadLocal<ThreadLocalSingleton>(){
        @Override
        protected ThreadLocalSingleton initialValue(){
            return new ThreadLocalSingleton();
        }
    };
    public static ThreadLocalSingleton getInstance(){
        return tlSingleton.get();
    }
}

三种破坏单例模式的方式:

1.反射

通过反射拿到私有的构造方法,设置setAccess()为true更改访问权限为public暴力初始化也能达到破坏单例的目的。
反射破坏单例的解决方案:在构造方法中添加限制条件,抛出异常阻止初始化

public class DestroySingletonReflex {
    private DestroySingletonReflex(){
        // 反射破坏单例的解决方案:在构造方法中添加判断,抛出异常
        // if (DestroySingletonReflexHolder.destroySingletonReflex != null){
        // throw new RuntimeException("不允许创建多个实例!");
        // }
    }
    private static class DestroySingletonReflexHolder{
        private static DestroySingletonReflex destroySingletonReflex = new DestroySingletonReflex();
    }
    public static DestroySingletonReflex getInstance(){
        return DestroySingletonReflexHolder.destroySingletonReflex;
    }
    /*
    通过反射破坏单例
     */
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        // 通过反射创建实例
        Class<?> clazz = DestroySingletonReflex.class;
        // 通过反射获取构造方法
        Constructor c = clazz.getDeclaredConstructor(null);
        c.setAccessible(true);

        // 暴力初始化
        Object o1 = c.newInstance();
        Object o2 = c.newInstance();

        System.out.println(o1);
        System.out.println(o2);
        System.out.println("是否相等:" + o1==o2);
    }
}
2.序列化和反序列化 

当我们将一个单例对象创建好,有时候需要将对象序列化然后写入到磁盘,下次使用时 再从磁盘中读取到对象,反序列化转化为内存对象。反序列化后的对象会重新分配内存, 即重新创建。那如果序列化的目标的对象为单例对象,就违背了单例模式的初衷,相当于破坏了单例。

反序列化破坏单例的本质是ObjectInputStream类的readObject()通过反射方式调用NewInstance()初始化实例。

反序列化破坏单例的解决方案:添加readResolve方法,改变checkResolve()结果阻止反射机制初始化实例。

public class DestroySingletonSerialize implements Serializable{
    private DestroySingletonSerialize(){
    }
    private static class DestroySingletonReflexHolder{
        private static DestroySingletonSerialize destroySingletonReflex = new DestroySingletonSerialize();
    }
    public static DestroySingletonSerialize getInstance(){
        return DestroySingletonReflexHolder.destroySingletonReflex;
    }
    public Object readResolve(){
        return getInstance();
    }
    /*
    通过序列化破坏单例
     */
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        DestroySingletonSerialize d1 = null;
        DestroySingletonSerialize d2 = DestroySingletonSerialize.getInstance();

        // 通过FileOutPutStream + ObjectOutPutStream将对象d2写到文件里进行序列化
        ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("tempFile"));
        outputStream.writeObject(d2);
        outputStream.flush();
        outputStream.close();

        // 通过FileInputStream和ObjectInputStream将文件中的对象读到d1中
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("tempFile"));
        d1 =(DestroySingletonSerialize) objectInputStream.readObject();
        System.out.println(d1);
        System.out.println(d2);
        System.out.println(d1==d2);
    }
}
3.原型模式调用clone()方法

原型破坏单例:实现Cloneable的单例类调用clone()复制实例是通过copy内存实现的,没有使用构造方法。

原型破坏单例的解决方案:不实现Cloneable接口。

public class DestorySingletonPrototype implements Cloneable{
    private DestorySingletonPrototype(){}
    private static class DestorySingletonPrototypeHolder{
        private static DestorySingletonPrototype singletonPrototype = new DestorySingletonPrototype();
    }
    public static DestorySingletonPrototype getInstance(){
        return DestorySingletonPrototypeHolder.singletonPrototype;
    }
    public static void main(String[] args) throws CloneNotSupportedException {
        DestorySingletonPrototype singletonPrototype = DestorySingletonPrototype.getInstance();
        DestorySingletonPrototype singletonPrototype1 = (DestorySingletonPrototype)singletonPrototype.clone();
        System.out.println(singletonPrototype1);
        System.out.println(singletonPrototype);
        System.out.println(singletonPrototype1 == singletonPrototype);
    }
}

使用场景:

  • Logger Classes
  • Configuration Classes
  • Accesing resources in shared mode
  • Factories implemented as Singletons

涉及单例模式的JDK源码:

  • 6
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Elaine202391

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值