当前位置:硬件测评 > 【java】单例模式深入解析

【java】单例模式深入解析

  • 发布:2023-10-02 02:07

概述

关于单例模式请看新手教程中的定义:

单例模式是Java中最简单的设计模式之一。这种类型的设计模式是一种创建模式,它提供了创建对象的最佳方式。此模式涉及一个类,该类负责创建自己的对象,同时确保只创建一个对象。此类提供了一种直接访问其独特对象的方法,而无需实例化该类的对象。

注意

  • 一个单例类只能有一个实例。
  • 单例类必须创建自己唯一的实例。
  • 单例类必须向所有其他对象提供此实例。

核心实现

饿男模式

包 com.dl.hungry_singleton; public class HungrySingleton {// 类加载时,单例对象已创建 private static final HungrySingleton instance = new HungrySingleton();// 构造函数私有化 private HungrySingleton() {}// 提供全局访问点 public static HungrySingleton newInstance () {返回实例;}
}

下面的实现方法与上面的代码基本相同。

包 com.dl.hungry_singleton; public class HungrySingleton {private static final HungrySingleton instance;//通过static块初始化static {instance = new HungrySingleton();}//构造函数 private HungrySingleton() {}//提供全局访问点 public static HungrySingleton newInstance() {return实例;}
}
  • 优点:简单、易读、能够保证绝对的线程安全;
  • 缺点:饥饿模式会浪费内存。如果整个开发系统中存在大量的Hungry模式单例对象,对系统是有害的。称之为灾难。
  • 解决方案:使用惰性模式;

懒惰模式

包com.dl.lazy_singleton;公共类 LazySimpleSingleton {私有静态 LazySimpleSingleton 实例; private LazySimpleSingleton() {}public static LazySimpleSingleton newInstance() {// 第一次使用该对象时,需要初始化 if (null == instance ) {instance = new LazySimpleSingleton();}return instance;}
}

对于有一点并发编程基础的同学来说,很容易看出上面的单例模式在多线程中存在问题。博主的测试代码就不列出来了。有兴趣的同学可以自己测试一下。下面仅作出结论并加以验证。

  • 多个线程调用newInstance()方法的返回值相同
    • 多个线程顺序执行,线程安全;
    • 如果不顺序执行,线程是不安全的; ① 假设两个线程已经通过 if ?结果是两个线程获得的对象将是相同的,因为后一个线程的新对象覆盖了前一个线程的新对象。虽然看上去是同一个对象,但实际上并不是线程安全的。 ?中断时,每个线程获得的对象的值是不同的。
  • 优点:解决Hungry Singleton模式的内存浪费问题
  • 缺点:线程不安全
  • 解决方案:对方法加锁,但在高并发系统中,会产生大量阻塞线程,性能很差 Bad

双重检查锁

包com.dl.lazy_singleton;公共类LazyDoubleCheckSingleton {私有易失性静态LazyDoubleCheckSingleton实例; private LazyDoubleCheckSingleton() {}public synchronized static LazyDoubleCheckSingleton newInstance() {//外层检查,控制阻塞情况,用于提高性能 if (null == instance) {synchronized(LazyDoubleCheckSingleton.class) {//内层检查,控制创建实例的条件 if (null == instance) {instance = new LazyDoubleCheckSingleton(); }}}返回实例;}
}

虽然这里也使用了synchronized关键字来加锁,但是在第一次初始化单例对象时会增加性能消耗;但毕竟系统运行时,创建单例对象在使用过程中需要花费大量的时间。占用的时间比例很小,所以加锁的成本是值得的;另外,锁的消耗只有在系统刚运行时使用多线程获取单例对象时才会发生。

  • 优点:保证线程安全,提高系统性能
  • 缺点:可读性差,代码不够优雅,双重if判断确实很难理解

静态内部类

包com.dl.lazy_singleton; public class LazyInnerClassSingleton {private LazyInnerClassSingleton() {}// 这里的方法是静态方法,所以内部类必须是静态内部类 public static LazyInnerClassSingleton getInstance() {return LazyInnerSingleton.LazySingleton;} private static class LazyInnerSingleton {private static最终 LazyInnerClassSingleton LazySingleton = new LazyInnerClassSingleton();}
}

相信第一次看到这个单例模型的同学可能会有些困惑。下面就让我们来揭晓答案;

类加载时机:类加载时机之一是当调用类中的静态属性或静态方法时,会首先判断类是否已经加载。如果没有,则首先加载该类。

静态内部类:当仅调用外部类的静态属性和静态方法时,不会加载静态内部类;仅当其他类或外部类调用静态内部类时 静态内部类仅在包含静态属性或静态方法时才被加载。

可以根据静态内部类的特点实现延迟加载机制。

  • 优点:代码简单,线程安全,线程安全由JVM的类加载机制保证
  • 缺点:前5种单例模式的实现都有一个共同的缺陷;私有权限构造函数不能阻止使用反射机制创建对象。
  • 解决方案:在构造函数中抛出异常,但这不符合编程的基本规则。
 private LazyInnerClassSingleton() {if (LazyInnerSingleton.LazySingleton != null) { throw new RuntimeException("无法反射地创建枚举对象");}}

Java API中有一个特殊的类叫做枚举(enum),它是上面代码中函数自带的。

注册单例枚举类

package com.dl.register_singleton;public enum EnumSingleton {INSTANCE;public static EnumSingleton getInstance() {return INSTANCE;}
}

枚举类是一个封装类,在jdk内部像数组一样实现。博主通过伪代码大致解释一下:

//上面枚举类的本质如下
EnumSingleton 类扩展了 Enum {public static Final EnumSingleton INSTANCE;
}
// jvm内部有一个系统维护的map,用于保存该类的所有枚举类对象
// map.put("INSTANCE", INSTANCE);

所以这也相当于第一种单例模式的改造,只不过枚举单例模式可以保证不会被反射机制破坏。

下面的测试是用来验证枚举类是否可以通过反射机制创建对象

 // 枚举类只有一个构造函数,所以测试时需要注意 protected Enum(String name, int ordinal) {www.sychzs.cn = name;this.ordinal = ordinal;}
package com.dl.register_singleton;import java.lang.reflect.Constructor;public class Test {public static void main(String[] args) {Class klass = EnumSingleton.class;try {//此处注意构造函数只有一个Constructor constructor = klass.getDeclaredConstructor(String.class, int.class);//设置构造函数的权限 constructor.setAccessible(true);//反射机制创建对象 Object obj = constructor.newInstance();} catch (Exception e) {e.printStackTrace();}}
}
java.lang.IllegalArgumentException:无法在 com.dl.register_singleton.Test.main(www.sychzs.cn:12) 处的 java.lang.reflect.Constructor.newInstance(www.sychzs.cn:417) 处反射创建枚举对象

显然,我们可以看到jdk不允许用户通过反射机制构造枚举对象。我们看一下www.sychzs.cn的源码:417

if ((clazz.getModifiers() & Modifier.ENUM) != 0) throw new IllegalArgumentException("无法反射地创建枚举对象");

至此可以得出结论,枚举单例的饥饿模式很好的保证了线程安全问题,可以保证不被反射机制破坏。这是jdk为我们保护的。因此,枚举单例模式应该是目前为止最好的实现方式。

容器化单例模式

package com.dl.register_singleton;导入www.sychzs.cn;
导入 java.util.concurrent.ConcurrentHashMap;public class ContainerSingleton {private static Map ioc = new ConcurrentHashMap<>();private ContainerSingleton() {}@SuppressWarnings("unchecked")public static  T getInstance( String className) {if (!ioc.containsKey(className)) {Object obj = null;try {obj = Class.forName(className);ioc.put(className, obj);} catch (ClassNotFoundException e) {e.printStackTrace ();}return (T) obj;} else {return (T) ioc.get(className);}}
}
  • 优点:枚举单例模式的升级(饿汉->懒汉),枚举单例模式下的map是由jdk控制的,而容器单例模式下的map是由用户类维护的,这样更方便方便灵活;
  • 缺点:会被反射机制破坏;

基于容器的单例模式其实是另一种设计思想。当开发的系统中有多个单例对象时,可以通过该模式进行保存。一个典型的例子就是Spring-ioc的底层javaBeanPool的实现。这部分可以看博主对Spring-ioc的模拟实现,这里不再赘述。

ThreadLocal 单例

包com.dl.threadLocal_singleton; public class ThreadLocalSingleton {private static final ThreadLocal threadLocal = new ThreadLocal() {@Overridepublic ThreadLocalSingleton get() {// 通过new对象获取,因为每个线程获取的对象不同 return new ThreadLocalSingleton();} ; };private ThreadLocalSingleton() {}public ThreadLocalSingleton getInstance() {return threadLocal.get();}
}

对于ThreadLocal源码的分析,博主稍后可能会发一篇博文来全面讲解。
这种单例模式可以避免锁定来实现同步。高并发下,可以保证每个线程都有自己唯一的对象,因此这不是单例模式实现的典型思路。这种方法虽然不能保证全局唯一性,但是可以保证线程唯一性。更好的应用场景是配合容器式的单例模式使用,只对对象中的单例对象(map等)进行操作,从而保证多线程的一致性。

  • 优点:保证线程安全,避免加锁带来的性能消耗,代码简单;
  • 缺点:用空间换时间,浪费内存,还会被反射机制破坏;

总结

这篇博文总共总结了7种典型的单例模式。它们都没有绝对的好或绝对的坏。通过不同的场景应用不同的模式,是程序员应该具备的最基本的素质。

相关文章