单例模式详解

本文详细介绍了Java中的单例模式,包括饿汉式、懒汉式、双检锁懒汉式和内部类懒汉式的实现,并分析了各方式的线程安全性和防止单例被破坏的方法。此外,还讨论了枚举单例的优势,以及如何通过反射、序列化和Unsafe破坏单例及其防御措施。
摘要由CSDN通过智能技术生成

单例模式详解

1. 概述

为什么要使用单例模式

在我们系统中,有一些对象其实我们只需要一个,比如说:线程池、缓存、对话框、注册表、日志对象、充当打印机、显卡等设备驱动程序的对象。事实上,这一类对象只能有一个实例,如果制造多个实例可能会导致一些问题的产生,比如说:程序的行为异常、资源使用过量、或者不一致性的结果。因此这里需要用到单例模式

使用单例模式的好处

  • 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级的对象而言,是非常可观的一笔系统开销。
  • 由于 new 操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻 GC 压力,缩短 GC 停顿时间。

2.饿汉式

2.1 饿汉式单例实现

实例会提前创建:

/**
 * @apiNote 饿汉式
 */
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()");
    }
    // 重写 readResolve 方法,如果序列化了,就会返回这个,不会返回反序列化的对象
    public Object readResolve() {
        return INSTANCE;
    }
}

测试:

public class TestSingleton1 {
    public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, IOException, ClassNotFoundException {
        // 触发 Singleton1 类的初始化,会为类的静态变量赋予正确的初始值,单例对象就会被创建!
        Singleton1.otherMethod();
        System.out.println("---------------------------");
        System.out.println(Singleton1.getInstance());
        System.out.println(Singleton1.getInstance());
    }
}

// 输出日志
private Singleton1()
otherMethod()
---------------------------
com.example.java.esign.mode.Singleton.Singleton1@5a42bbf4
com.example.java.esign.mode.Singleton.Singleton1@5a42bbf4


2.2 破坏单例的几种情况

  1. 反射破坏单例
  2. 返序列化破坏单例
  3. Unsafe 破坏单例

演示:

public class TestSingleton1 {
    public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, IOException, ClassNotFoundException {
        // 触发 Singleton1 类的初始化,会为类的静态变量赋予正确的初始值,单例对象就会被创建!
        Singleton1.otherMethod();
        System.out.println("---------------------------");
        System.out.println(Singleton1.getInstance());
        System.out.println(Singleton1.getInstance());

        // 反射破坏单例
        reflection(Singleton1.class);
        //反序列化破坏单例
        serializable(Singleton1.getInstance());
        //Unsafe 破坏单例 ,无法预防
        unsafe(Singleton1.class);
    }
    // 反射破坏单例
    private static void reflection(Class<?> clazz) throws NoSuchMethodException, InvocationFailureException, InstantiationException, IllegalAccessException, InvocationTargetException {
        // 得到无参
        Constructor<?> constructor = clazz.getDeclaredConstructor();
        // 将此对象的 accessible 标志设置为指示的布尔值,即设置其可访问性
        constructor.setAccessible(true);
        // 创建实例
        System.out.println("反射创建实例: " + constructor.newInstance());
    }

    // 反序列化破坏单例
    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());
    }
    // Unsafe 破坏破例
    private static void unsafe(Class<?> clazz) throws InstantiationException {
        Object o = UnsafeUtils.getUnsafe().allocateInstance(clazz);
        System.out.println("Unsafe 创建实例: " + o);
    }
}


// 输出日志
private Singleton1()
otherMethod()
---------------------------
com.example.java.esign.mode.Singleton.Singleton1@5a42bbf4
com.example.java.esign.mode.Singleton.Singleton1@5a42bbf4
private Singleton1()
反射创建实例: com.example.java.esign.mode.Singleton.Singleton1@270421f5
反序列化创建实例: com.example.java.esign.mode.Singleton.Singleton1@7ac7a4e4
Unsafe 创建实例: com.example.java.esign.mode.Singleton.Singleton1@56f4468b

可以看出三种方式都会破坏单例 !

2.3 预防单例的破坏

2.3.1 预防反射破坏单例

在构造方法中加个判断即可:

// 构造私有
private Singleton1() {
    // 防止反射破坏单例
    if(INSTANCE != null) {
        throw new RuntimeException("单例对象不能重复创建");
    }
    System.out.println("private Singleton1()");
}  
2.3.2 预防反序列化破坏单例

Singleton1()中重写 readResolv() 方法

// 重写 readResolve 方法,如果序列化了,就会返回这个,不会返回反序列化的对象
public Object readResolve() {
    return INSTANCE;
}
2.3.3 Unsafe 破坏单例无法预防

3. 枚举饿汉式

3.1 枚举单例实现

枚举实现单例:

/**
 * @apiNote 枚举实现单例
 */
public enum Singleton2 {
    INSTANCE;

    // 枚举的构造方法默认是 private 的,可以不写
    Singleton2() {
        System.out.println("private Singleton2()");
    }
    // 重写 toString 方法
    @Override
    public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }

    // 获得实例方法(这个方法可以不要,枚举变量都是 public 的)
    public static Singleton2 getInstance() {
        return INSTANCE;
    }

    // 其他方法
    public static void otherMethod() {
        System.out.println("otherMethod()");
    }
}

测试:

public class TestSingleton2 {
    public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, IOException, ClassNotFoundException {
        // 触发 Singleton1 类的初始化,会为类的静态变量赋予正确的初始值,单例对象就会被创建!
        Singleton2.otherMethod();
        System.out.println("---------------------------");
        System.out.println(Singleton2.getInstance());
        System.out.println(Singleton2.getInstance());
    }
}

// 输出:
private Singleton2()
otherMethod()
---------------------------
com.example.java.esign.mode.Singleton.Singleton2@5a42bbf4
com.example.java.esign.mode.Singleton.Singleton2@5a42bbf4

可以看出当调用 otherMethod() 时,就会触发类的加载,枚举对象就会创建,所以枚举实现单例是饿汉式的

3.2 破坏单例

枚举类实现单例的好处:

  1. 反序列化无法破坏枚举单例
  2. 反射无法破坏枚举单例

例子:

需要先修改反射破坏代码,枚举需要有构造

public class TestSingleton2 {
    public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, IOException, ClassNotFoundException {
        // 触发 Singleton2 类的初始化,会为类的静态变量赋予正确的初始值,单例对象就会被创建!
        Singleton2.otherMethod();
        System.out.println("---------------------------");
        System.out.println(Singleton2.getInstance());
        System.out.println(Singleton2.getInstance());

        // 反序列化破坏单例,无法破坏枚举单例
        serializable(Singleton2.getInstance());

        // Unsafe 破坏单例 ,无法预防
        unsafe(Singleton2.class);

        // 反射是无法创建枚举对象,无法破坏枚举单例
        reflection(Singleton2.class);
    }
    // Unsafe 破坏破例
    private static void unsafe(Class<?> clazz) throws InstantiationException {
        Object o = UnsafeUtils.getUnsafe().allocateInstance(clazz);
        System.out.println("Unsafe 创建实例: " + o);
    }
    // 反序列化破坏单例
    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());
    }

    // 反射破坏单例
    private static void reflection(Class<?> clazz) throws NoSuchMethodException, InvocationFailureException, InstantiationException, IllegalAccessException, InvocationTargetException {
        // 得到无参
        Constructor<?> constructor = clazz.getDeclaredConstructor(String.class,int.class);
        // 将此对象的 accessible 标志设置为指示的布尔值,即设置其可访问性
        constructor.setAccessible(true);
        // 创建实例
        System.out.println("反射创建实例: " + constructor.newInstance());
    }
}

// 输出结果:

private Singleton2()
otherMethod()
---------------------------
com.example.java.esign.mode.Singleton.Singleton2@5a42bbf4
com.example.java.esign.mode.Singleton.Singleton2@5a42bbf4
反序列化创建实例: com.example.java.esign.mode.Singleton.Singleton2@5a42bbf4
Unsafe 创建实例: com.example.java.esign.mode.Singleton.Singleton2@6fb554cc
    
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
	at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
	at com.example.java.esign.mode.Singleton.TestSingleton2.reflection(TestSingleton2.java:55)
	at com.example.java.esign.mode.Singleton.TestSingleton2.main(TestSingleton2.java:30)

可以看出

1. **反射是无法创建枚举对象! 无法破坏枚举单例**
2. **反序列化也不会破坏枚举单例 !**
3. **Unsafe 依然会破坏 !**

4. 懒汉式

实现代码如下:

/**
 * @apiNote 懒汉式
 * 只有在第一次调用 getInstance() 时才会创建唯一的单例对象,因此是懒汉式
 */
public class Singleton3 implements Serializable {
    // 构造私有
    private Singleton3() {
        System.out.println("private Singleton3");
    }

    // 唯一实例
    private static Singleton3 INSTANCE = null;
    // 在没加入 synchronized ,在多线程环境下是会有问题的,可能多个线程会同时执行 INSTANCE = new Singleton3();
    // 因此这里需要在 getInstance() 方法上加上 synchronized 关键字保证多线程下的正确性
    // 但是这种方法是有问题的,第一次创建完对象后,以后的操作是不需要在加锁的,所以这种方式会影响性能!
    // 我们的目标应该是第一次创建单例的时候给予保护,后续操作则不需要加锁保护!
    public static Singleton3 getInstance() {
        // 第一次调用的时候才创建
        if (INSTANCE == null) {
            INSTANCE = new Singleton3();
        }
        return INSTANCE;
    }

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

测试 :

public class TestSingleton3 {
    public static void main(String[] args) {
        Singleton3.otherMethod();
        System.out.println("----------------------");
        System.out.println(Singleton3.getInstance());
        System.out.println(Singleton3.getInstance());
    }
}

// 输出日志
otherMethod()
----------------------
private Singleton3()
com.example.java.esign.mode.Singleton.Singleton3@5a42bbf4
com.example.java.esign.mode.Singleton.Singleton3@5a42bbf4

可以看出只有第一次调用 getInstance() 时才会创建唯一的单例对象,因此是懒汉式。

但这种方式在多线程环境下是有问题的,可能多个线程会同时执行 INSTANCE = new Singleton3();。 因此这里需要在 getInstance() 方法上加上 synchronized 关键字保证多线程下的正确性:

 public static synchronized Singleton3 getInstance() {
     // 第一次调用的时候才创建
     if (INSTANCE == null) {
         INSTANCE = new Singleton3();
     }
     return INSTANCE;
 }

但是这种方法是有问题的,第一次创建完对象后,以后的操作不需要再加锁的,所以这种方式会影响性能!

我们的目标应该是第一次创建单例的时候给予保护,后续操作则不需要加锁保护!

5.双检锁懒汉式

针对上面的问题,这里给出第四种方法双检锁懒汉式进行优化:

/**
 * @apiNote 双检锁懒汉式
 */
public class Singleton4 implements Serializable {
    // 构造私有
    private Singleton4() {
        System.out.println("private Singleton4()");
    }

    // 唯一实例
    private static Singleton4 INSTANCE = null;

    // 双检锁优化
    public static synchronized Singleton4 getInstance() {
        // 实例没创建,才会进入内部的 synchronized 代码块,提高性能,防止每次都加锁
        if (INSTANCE == null) {
            // 可能第一次线程在 synchronized 代码块还没创建完对象时,第二个线程已经到了这一步,所以里面还需要加上判断
            synchronized (Singleton4.class) {
                // 也许有其他线程已经创建实例,所以再判断一次
                if (INSTANCE == null) {
                    INSTANCE = new Singleton4();
                }
            }
        }
        return INSTANCE;
    }
    // 其他方法
    public static void otherMethod() {
        System.out.println("otherMethod()");
    }
}

6. 内部类懒汉式

内部类懒汉式单例实现:

/**
 * @apiNote 内部类懒汉式
 */
public class Singleton5 {
    // 构造私有
    private Singleton5() {
        System.out.println("private Singleton5()");
    }
    // 静态内部类实现懒汉式单例,静态变量的创建会放在静态代码块里执行,jvm 会保证其他线程安全
    // 只有第一次用到内部类时,才会初始化创建单例
    private static class Holder {
        static Singleton5 INSTANCE = new Singleton5();
    }

    // 获得实例方法
    public static Singleton5 getInstance() {
        return Holder.INSTANCE;
    }

    // 其他方法
    public static void otherMethod() {
        System.out.println("otherMethod()");
    }
}

测试:

public class TestSingleton5 {
    public static void main(String[] args) {
        Singleton5.otherMethod();
        System.out.println("----------------------");
        System.out.println(Singleton5.getInstance());
        System.out.println(Singleton5.getInstance());
    }
}

// 输出
otherMethod()
----------------------
private Singleton5()
com.example.java.esign.mode.Singleton.Singleton5@270421f5
com.example.java.esign.mode.Singleton.Singleton5@270421f5

可以看出内部类实现单例也是懒汉式的。

本文抄录至 https://blog.csdn.net/qq_45966440/article/details/122148123

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值