设计模式 | 单例模式 | 五种代码实现及三种破坏实验 | 无知的我学习日记(图文排版无水印)

无知的我正在学习设计模式中的单例模式。。。

如果有遇到有任何无法进展问题或者疑惑的地方,应该在讨论区留言 或者 其他途径以寻求及时的帮助,以加快学习效率 或者 培养独立解决问题的能力、扫清盲点、补充细节

单例模式-饿汉式

是什么

一个类只能创建一个实例对象

如何实现

  1. 准备一个类
  2. 定义一个无参私有构造方法。//为了其不能被调用而被重写
  3. 创建当前类的实例,并赋给一个私有的、静态的、最终的成员变量
  4. 定义一个静态的方法,并编写这个方法,使得这个方法被调用时会返回第三步创建的类实例

代码实现-定义一个饿汉式

public class Singleton1 implements Serializable {
    private Singleton1() {
        System.out.println("private Singleton1()");
    }

    private static final Singleton1 INSTANCE = new Singleton1();

    public static Singleton1 getInstance() {
        return INSTANCE;
    }

    public static void otherMethod() {
        System.out.println("otherMethod()");
    }
}

引出问题-该类实例是在什么时候被创建的?

在该类被触发的时候,就是调用该类的时候。这是因为在调用时,该类的被静态修饰的会被自动地调用,从而创建了该类的实例

误区是 在调用 能够返回该类实例的方法 的时候。

举例说明 上一个问题

image-20220503101246477

可看见

  • 当调用Singleton1.otherMethod()时,类的实例已经被创建。所以下面语句输出的是同一个对象

破坏用反射

原理

  1. 获取类对象
  2. 根据类对象获取其无参构造方法
  3. 让该无参构造方法可以被访问
  4. 通过无参构造方法创建新的对象

代码实现

package day01.pattern;

import org.springframework.objenesis.instantiator.util.UnsafeUtils;

import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class TestSingleton {
    public static void main(String[] args) throws Exception {
        Singleton5.otherMethod();
        System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
        System.out.println(Singleton5.getInstance());
        System.out.println(Singleton5.getInstance());

        // 反射破坏单例 1
        reflection(Singleton3.class);
    }
	// 反射破坏单例 2
    private static void reflection(Class<?> clazz) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
        Constructor<?> constructor = clazz.getDeclaredConstructor();//根据传入的类对象得到该类的无参构造方法
        constructor.setAccessible(true);//通过上一条语句获得的无参构造方法,让私有的构造方法也可以被使用
        System.out.println("反射创建实例:" + constructor.newInstance());//通过无参构造方法,创建对象
    }
}

运行结果如下图

image-20220503101934222

解决是 在该类构造方法中,写一个判断该类实例是否已经创建方法的条件,如果已经创建有,则抛出异常

public class Singleton1 implements Serializable {
    private Singleton1() {
        //解决
        if (INSTANCE != null) {
            throw new RuntimeException("单例对象不能重复创建");
        }
        System.out.println("private Singleton1()");
    }

    private static final Singleton1 INSTANCE = new Singleton1();

    public static Singleton1 getInstance() {
        return INSTANCE;
    }

    public static void otherMethod() {
        System.out.println("otherMethod()");
    }
}

破坏用反序列化

原理

  1. 获取单例对象
  2. 把对象转换为字节流
  3. 通过反序列化的字节数组创建新的对象
  4. 把字节流还原为对象

代码实现

前提是 饿汉类实现了序列化

package day01.pattern;

import org.springframework.objenesis.instantiator.util.UnsafeUtils;

import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class TestSingleton {
    public static void main(String[] args) throws Exception {
        Singleton5.otherMethod();
        System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
        System.out.println(Singleton5.getInstance());
        System.out.println(Singleton5.getInstance());

        // 反序列化破坏单例
        serializable(Singleton3.getInstance());

    }

    private static void serializable(Object instance) throws IOException, ClassNotFoundException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(instance);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
        System.out.println("反序列化创建实例:" + ois.readObject());
    }
}

运行结果如下图

image-20220503102556949

解决是 在饿汉类中,重写一个返回值为object的readResolve方法

public class Singleton1 implements Serializable {
    private Singleton1() {
        System.out.println("private Singleton1()");
    }

    private static final Singleton1 INSTANCE = new Singleton1();

    public static Singleton1 getInstance() {
        return INSTANCE;
    }

    public static void otherMethod() {
        System.out.println("otherMethod()");
    }

    public Object readResolve() {
        return INSTANCE;
    }
}

这是因为当通过反序列化创建该类实例时,会首先执行该方法来返回已经创建的实例

破坏用unsafe

原理

利用unsafe获取类对象而直接创建新的实例对象

代码实现

package day01.pattern;

import org.springframework.objenesis.instantiator.util.UnsafeUtils;

import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class TestSingleton {
    public static void main(String[] args) throws Exception {
        Singleton5.otherMethod();
        System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
        System.out.println(Singleton5.getInstance());
        System.out.println(Singleton5.getInstance());

        // Unsafe 破坏单例
        unsafe(Singleton3.class);
    }
    private static void reflection(Class<?> clazz) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
        Constructor<?> constructor = clazz.getDeclaredConstructor();
        constructor.setAccessible(true);
        System.out.println("反射创建实例:" + constructor.newInstance());
    }
}

解决是 暂时没有

单例模式-枚举饿汉式

实现

代码实现

public enum Singleton2 {
    INSTANCE;

    private Singleton2() {
        System.out.println("private Singleton2()");
    }

    @Override
    public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }

    public static Singleton2 getInstance() {
        return INSTANCE;
    }

    public static void otherMethod() {
        System.out.println("otherMethod()");
    }
}

反序列化破坏失效。这是因为,当进行反序列化的字节数组想要创建新对象时,如果遇到了枚举类,则会直接返回已经创建的实例

原理

package day01.pattern;

enum Sex {
    MALE, FEMALE;
}
//下面是经过翻译后的当前类生成的class文件
/*final class Sex extends Enum<Sex> {
    public static final Sex MALE;
    public static final Sex FEMALE;

    private Sex(String name, int ordinal) {
        super(name, ordinal);
    }

    static {
        MALE = new Sex("MALE", 0);
        FEMALE = new Sex("FEMALE", 1);
        $VALUES = values();
    }

    private static final Sex[] $VALUES;

    private static Sex[] $values() {
        return new Sex[]{MALE, FEMALE};
    }

    public static Sex[] values() {
        return $VALUES.clone();
    }

    public static Sex valueOf(String value) {
        return Enum.valueOf(Sex.class, value);
    }
}*/

破坏用反射

代码实现

package day01.pattern;

import org.springframework.objenesis.instantiator.util.UnsafeUtils;

import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class TestSingleton {
    public static void main(String[] args) throws Exception {
        Singleton5.otherMethod();
        System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
        System.out.println(Singleton5.getInstance());
        System.out.println(Singleton5.getInstance());

        // 反射破坏单例
        reflection(Singleton3.class);

    private static void reflection(Class<?> clazz) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
        Constructor<?> constructor = clazz.getDeclaredConstructor(String.class, int.class);
        constructor.setAccessible(true);
        System.out.println("反射创建实例:" + constructor.newInstance("OTHER", 1));
    }
}

结果是 反射破坏失效,会抛出异常。这是因为同反序列化的原因类似

破坏用unsafe

代码实现

package day01.pattern;

import org.springframework.objenesis.instantiator.util.UnsafeUtils;
import sun.misc.Unsafe;

// 示例:通过 Unsafe 造出一个 Enum 对象
public class EnumCreator {
    public static void main(String[] args) throws Exception {
        Unsafe unsafe = UnsafeUtils.getUnsafe();
        long nameOffset = unsafe.objectFieldOffset(Enum.class.getDeclaredField("name"));
        long ordinalOffset = unsafe.objectFieldOffset(Enum.class.getDeclaredField("ordinal"));
        Sex o = (Sex) unsafe.allocateInstance(Sex.class);
        unsafe.compareAndSwapObject(o, nameOffset, null, "阴阳人");
        unsafe.compareAndSwapInt(o, ordinalOffset, 0, 2);
        System.out.println(o.name());
        System.out.println(o.ordinal());
    }

}

结果是 破坏失败

单例模式-懒汉式

代码实现

public class Singleton3 implements Serializable {
    private Singleton3() {
        System.out.println("private Singleton3()");
    }

    private static Singleton3 INSTANCE = null;

    // Singleton3.class
    public static Singleton3 getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton3();
        }
        return INSTANCE;
    }

    public static void otherMethod() {
        System.out.println("otherMethod()");
    }

}

引出问题 当多线程时,有可能破环单例

解决办法是 给这个方法加上一把锁(就是synchronized修饰符)。如下

public class Singleton3 implements Serializable {
    private Singleton3() {
        System.out.println("private Singleton3()");
    }

    private static Singleton3 INSTANCE = null;

    // Singleton3.class
    public static synchronized Singleton3 getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton3();
        }
        return INSTANCE;
    }

    public static void otherMethod() {
        System.out.println("otherMethod()");
    }

}

引出问题 加锁加导致性能降低。这是因为只需要在第一次并发时,才需要加锁维护,但是在创建实例完成后,就不需要了。

单例模式-DCL懒汉式

双检锁懒汉式

解决上一个问题的办法是 在加锁前再做一次判断

public class Singleton4 implements Serializable {
    private Singleton4() {
        System.out.println("private Singleton4()");
    }

    private static volatile Singleton4 INSTANCE = null; // 可见性,有序性 @volatile 必须加

    public static Singleton4 getInstance() {
        if (INSTANCE == null) {//在这再加一个判断
            synchronized (Singleton4.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Singleton4();
                }
            }
        }
        return INSTANCE;
    }

    public static void otherMethod() {
        System.out.println("otherMethod()");
    }
}

容易产生的疑惑:为什么内部还需要一次判断呢?可以假设没有内部判断,做代码模拟,可知道会发生多线程问题。

单例模式-DCL懒汉式为何加volatile

INSTANCE = new Singleton4() 不是原子的,分成 3 步:创建对象、调用构造、给静态变量赋值,其中后两步可能被指令重排序优化,变成先赋值、再调用构造

如果线程1 先执行了赋值,线程2 执行到第一个 INSTANCE == null 时发现 INSTANCE 已经不为 null,此时就会返回一个未完全构造的对象

演示图像

出现错误的情况

image-20220503132741315

添加后volatile

image-20220503132907916

饿汉式、枚举饿汉式为什么不需要考虑多线程问题?

这是因为创建的对象赋值给了静态变量,静态变量储存在静态代码块,静态代码块自动加锁

单例模式-内部懒汉式

public class Singleton5 implements Serializable {
    private Singleton5() {
        System.out.println("private Singleton5()");
    }

    private static class Holder {
        static Singleton5 INSTANCE = new Singleton5();
    }

    public static Singleton5 getInstance() {
        return Holder.INSTANCE;
    }

    public static void otherMethod() {
        System.out.println("otherMethod()");
    }
}

好处是 既防止了多线程问题,又防止了效率降低(第一次访问时才用到)

  • 10
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 9
    评论
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值