001设计模式:单例模式

    科技2026-03-20  8

    单例模式的定义: 单例模式是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。隐藏其所有的构造方法。

    创建模式的常见写法

    1:饿汉模式

    2:懒汉模式

    3:注册模式

    4:ThreadLocal单例

    饿汉模式

    饿汉模式单例是单例首次加载时就创建实例

    /*** * 1)私有构造函数 * * 2)静态私有成员--在类加载时已初始化 * * 3)公开访问点getInstance-----不需要同步,因为在类加载时已经初始化完毕, * 也不需要判断null,直接返回 * * 缺点:不管用没用到类 都会给你初始化 浪费类型空间 */ public class HungrySingleton implements Serializable { /** * 饿汉模式 直接初始化 */ private static final HungrySingleton hungrySingleton = new HungrySingleton(); /** * 私有化 构造方法 别人调用不到 */ private HungrySingleton() {} public static HungrySingleton getInstance(){ return hungrySingleton; } }

    缺点:不管用没用到实例,都会初始化,浪费类型空间。

    破坏单例的方式有序列化单例。代码如下:

    /** * 序列化去创建单例 * * @param args */ public static void main(String[] args) { HungrySingleton s1 = null; HungrySingleton s2 = HungrySingleton.getInstance(); FileOutputStream fos = null; try { fos = new FileOutputStream("HungrySingleton.obj"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(s2); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream("HungrySingleton.obj"); ObjectInputStream ois = new ObjectInputStream(fis); s1 = (HungrySingleton)ois.readObject(); ois.close(); System.out.println("s1:"+s1); System.out.println("s2"+s2); System.out.print("s1和s2的比较为"); System.out.println(s1 == s2); } catch (Exception e) { e.printStackTrace(); } }

    执行结果为false,这是因为序列化的时候再次初始化了单例。

    源码分析:

    ois.readObject();

    -> 431行 readObject(false)

    ->readOrdinaryObject(unshared)

    ->2053行 obj = desc.isInstantiable() ? desc.newInstance() : null;

    desc.isInstantiable() -> 1023 return (cons != null); //判断是否有构造方法如果有返回 true

    这里又再次初始化了一次实体类

    为了防止这种事件发生 需要在实体类中添加方法

    private Object readResolve(){ return hungrySingleton; }

    源码分析:

    ois.readObject();

    -> 431行 readObject(false)

    ->1573行 readOrdinaryObject(unshared)

    ->2074行

    obj != null &&handles.lookupException(passHandle) == null &&

    desc.hasReadResolveMethod()

    前两个判断条件为true 第三个判断条件 hasReadResolveMethod() 字面意思就是看有没有 readResolve 这个方法 如果有 继续往下走

    ->2091行 handles.setObject(passHandle, obj = rep);

    obj :为反序列话时候再次初始化的值

    rep:为执行readResolve方法返回的值

    源码分析可以看出实际上创建了两次,只不过初始化的那个让GC处理了

    懒汉式单例

    被外部类调用时才创建实例

    /** * 懒加载: * 1) 被外部类调用的时候才会创建实例 * 2)私有构造函数 */ public class LazySingleton implements Serializable{ private static LazySingleton lazySingleton; private LazySingleton() {} /** * 为了线程安全和性能 把synchronized 放到方法里边 * * @return */ public static LazySingleton getInstance() { if (lazySingleton == null) { // 如果为空创建 synchronized (LazySingleton.class) { /** * 这个时候在去判空一次 * 防止当lazySingleton 为空的时候 两个线程 同时进入这里 */ if (lazySingleton == null) { lazySingleton = new LazySingleton(); } } } return lazySingleton; } }

    注意:这里用到双重检查锁

    还有一种懒加载方法:静态内部类,静态内部类是最高级的单例。

    静态内部类特性:只有在外部调用的时候,才会加载。

    public class LazyInnerClassSingleton { private static final LazyInnerClassSingleton lazyInnerClassSingleton = new LazyInnerClassSingleton(); private LazyInnerClassSingleton() { if (lazyHolder.LAZY != null) { throw new RuntimeException("不允许创建"); } } /** * @return */ public static LazyInnerClassSingleton getInstance() { return lazyHolder.LAZY; } /** * 静态内部类 * 静态内部类 是最高级的 单例模式 * 巧妙的利用了内部类的特性 * 只有在外部类调用的时候 才会去创建内部类 */ private static class lazyHolder { private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton(); } }

    上边代码虽然私有化了构造方法,但是在反射的时候,jvm不管是不是私有化,他会执行他的初始方法。所以在初始化方法加上报错,防止被多次创建。

    反射测试代码如下:

    /** * 静态内部类有 可能 被反射攻击 * 通过反射去获取他的 构造方法 */ public static void main(String[] args) { EnumSingleton instance = EnumSingleton.INSTANCE; LazyInnerClassSingleton lazyInnerClassSingleton = LazyInnerClassSingleton.getInstance(); // 获取他的构造方法 Class<?> lazyClass = LazyInnerClassSingleton.class; try { Constructor c = lazyClass.getDeclaredConstructor(null); c.setAccessible(true); LazyInnerClassSingleton o = (LazyInnerClassSingleton) c.newInstance(); // 比较 o和 一开始创建的 不是同一个变量 System.out.print("o和lazyInnerClassSingleton比较结果为: "); System.out.println(o == lazyInnerClassSingleton); } catch (Exception e) { e.printStackTrace(); } } }

    注册式单例

    将每一个实例都缓存到统一的容器中,使用唯一表示获取实例。

    最常见的如,spring ioc容器

    public class ContainerSngleton { private ContainerSngleton() { } private static Map<String, Object> ioc = new ConcurrentHashMap<String, Object>(); public static Object getBean(String beanName) { if (!ioc.containsKey(beanName)) { synchronized (ContainerSngleton.class) { if (!ioc.containsKey(beanName)) { return ioc.get(beanName); } Object obj = null; try { obj = Class.forName(beanName).newInstance(); ioc.put(beanName, obj); } catch (Exception e) { e.printStackTrace(); } } } return ioc.get(beanName); } }

    注册容器里还有枚举式单例:如

    /** * 注册式单例 * * 枚举式单例 这种是线程安全的 * * 因为在 通过工具查看指令时候会发现 */ public enum EnumSingleton { INSTANCE; public Object getData() { return data; } public void setData(Object data) { this.data = data; } private Object data; public static EnumSingleton getInstance(){ return INSTANCE; } }

    用jad 反编译 class 文件可以看到 如下:

    (jad下载链接https://varaneckas.com/jad/jad158g.win.zip)

    static { INSTANCE = new EnumSingleton("INSTANCE", 0); $VALUES = (new EnumSingleton[] { INSTANCE }); }

    对没错,饿汉式。jvm编译了一个static 静态方法 把里边的实例 初始化到一个数组里边。

    反射源码分析枚举初始化:

    newInstance()方法 416行

    if ((clazz.getModifiers() & Modifier.ENUM) != 0) throw new IllegalArgumentException(“Cannot reflectively create enum objects”); 这里如果是枚举类 调用反射初始化方法,会报错。

    从jdk层面就为枚举不被序列化和反射破坏来保驾护航

    原型模式(Prototype)

    通俗点讲就是创建一个实体类A,然后通过new 的方式 复制一份A出来,因为是new 出来的,A和B的值相等,但是内存地址和属性内存地址不同。又因为A和B是同一实体类,所以A和B保留着完整的配置中依赖关系。这也就是Spring中通常用到的原型模式(scope = “prototype”) 在每次使用对象之前,都会创建一个新的对象,并且将依赖关系完整的复制给这个新创建的对象。

    java 中提供Cloneable接口,但这是潜克隆

    public class ClonSign implements Cloneable, Serializable { public Student s = new Student(); @SneakyThrows @Override public Object clone() throws CloneNotSupportedException { return super.clone(); }

    通过new 的当时创建一个ClonSign对象然后调用clone方法获取到的对象,前后两个对象的地址不同,但是两个对象里边的属性Student是地址相同,是同一个对象。

    要想实现深克隆常用的方法有序列化:

    public class ClonSign implements Cloneable, Serializable { public Student s = new Student(); @SneakyThrows @Override public Object clone() throws CloneNotSupportedException { ClonSign s1 = null; FileOutputStream fos = null; fos = new FileOutputStream("HungrySingleton.obj"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(this); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream("HungrySingleton.obj"); ObjectInputStream ois = new ObjectInputStream(fis); s1 = (ClonSign)ois.readObject(); return s1; } }

    通过反射去获取一个新的对象,这样就可以实现我们的深克隆,产生的对象和对象里边的属性为不同的实例。(写到这里,翻到头去看,这不正是我们,序列化破坏单例的例子嘛。)

    彩蛋:

    用ThreadLocal去初始化单例

    public class ThreadLocalSingleton { private ThreadLocalSingleton() {} private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance = new ThreadLocal<ThreadLocalSingleton>() { @Override protected ThreadLocalSingleton initialValue() { return new ThreadLocalSingleton(); } }; public static ThreadLocalSingleton getInstance(){ return threadLocalInstance.get(); } }

    然后用多线程去调用getInstance()方法,你会发现在同一个线程里边获取到的单例是同一个对象,不同线程获取到的单例是不同对象。

    源码分析:

    threadLocalInstance.get()

    -> Thread t = Thread.currentThread();

    ThreadLocalMap map = getMap(t);

    if (map != null) {

    ThreadLocalMap.Entry e = map.getEntry(this);

    通过源码分析他是线程间的安全,在一个线程中去获取他是线程安全的,在不同线程线程间他的,对象是不相等的,也可以叫做伪线程安全,通过源码 可以发现他是以当前线程作为key

    单例模式的优点:

    在内存中只有一个实例,减少了内存的开销.

    可以避免堆资源的多重占用。

    设置全局访问点,严格控制访问

    Processed: 0.009, SQL: 9