经典面试题-详解单例模式

单例模式

单例模式有以下特点:  
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。

  1. 饿汉式单例
public class Singleton1 implements Serializable {
    //构造私有(所有的单例模式构造都得私有)
    private Singleton1() {
        //下面这个if  预防反射破坏单例
        /*if (INSTANCE != null) {
            throw new RuntimeException("单例对象不能重复创建");
        }*/
        System.out.println("private Singleton1()");
    }

    //赋值给静态变量,最终放在静态代码块中执行,静态代码块的线程安全由jvm保证了线程安全;
    //想办法将对象的创建放入静态代码块就是线程安全的,即方式5
    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;
    }*/
}

上面的代码就是要一个简单的饿汉式的单例模式,可以配合TestSingleton.java来测试单例模式。

  • 饿汉式单例(在没有调用getInstance()时 实例也会被提前创建,初始化时就会被创建 (加载,链接,初始化三个动作)) 在类加载的时候会初始化对象,所以饿汉式的实例化时机就是类加载时机。
    1. 构造私有(所有的单例模式构造都得私有)
    2. 提供静态的成员变量
    3. 提供公共的静态方法获取实例
      饿汉式单例不会有线程安全问题,因为private static final Singleton1 INSTANCE = new Singleton1();赋值给静态变量最终是在静态代码块中执行的,而静态代码块的线程安全由jvm保证了。但是坏处是浪费内存空间 (不管你用不用都创建了)。

单例模式可能会被破坏:

  • 反序列化破坏单例
    因为该类可能需要在保存磁盘或者网络中传输,需要实现Serializable接口,但是实现了该接口之后可能会破坏单例模式,可以再在TestSingleton.java来测试。
    解决:
/**
 * readResolve() 预防 反序列化破坏单例 (固定方法名的)
 */
public Object readResolve() {
    return INSTANCE;
}
  • 反射破坏单例
    可以在私有构造中添加如下代码
//下面这个if  预防反射破坏单例
if (INSTANCE != null) {
    throw new RuntimeException("单例对象不能重复创建");
}
  • Unsafe破坏单例
    目前好像没有解决方法?
  1. 枚举饿汉式
/**
* 枚举类实现饿汉模式只需要下面一行即可
public enum Singleton2 {    
    INSTANCE;
}
enum:从jdk1.5之后提供的一个关键字,用于定义枚举类;
Enum:是一个抽象类,所有使用enum关键字定义的类默认继承了该类Enum 实现了Serializable接口
*/
public enum Singleton2 {
    //枚举变量最终也是在静态代码块中创建枚举变量对应实例对象;静态代码块线程安全由jvm保证了线程安全;
    INSTANCE;

    //以下代码都是为了测试需要    枚举的构造默认就是私有的
    private Singleton2() {
        System.out.println("private Singleton2()");
    }

    @Override
    public String toString() {
        //默认只会打印 INSTANCE
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }

    /**
     * 可以没有,因为枚举变量都是公共的
     * @return
     */
    public static Singleton2 getInstance() {
        return INSTANCE;
    }

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

单例模式可能会被破坏:

  • 不怕反序列化破坏单例(ObjectInputStream 会对枚举类做特殊处理,遇到枚举类会直接将枚举类实例直接返回)
  • 不怕 反射破坏单例 (没办法通过反射创建枚举对象,会直接报错Cannot reflectively create enum objects)
  • unsafe还是可以破坏单例的

枚举实现的饿汉模式也不会有线程安全问题;而且非常的简洁;也不会有因为反序列化和反射来破坏单例

  1. 懒汉式单例
public class Singleton3 implements Serializable {
    private Singleton3() {
        System.out.println("private Singleton3()");
    }

    private static Singleton3 INSTANCE = null;

    public static Singleton3 getInstance() {
    // 加载静态方法上的synchronized 就是给 类.class 加锁
    //public static synchronized Singleton3 getInstance() {
        if (INSTANCE == null) { //有线程安全问题  需要加上 synchronized 关键字即可,虽然能解决问题但是性能不好:只有首次才会有问题,后续就没问题了;最好
            //是首次创建单例时提供线程安全的保护,后续就不需要加 synchronized,此时就是 DCL懒汉式
            INSTANCE = new Singleton3();
        }
        return INSTANCE;
    }

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

}

当第一次调用getInstance()时才会创建这个实例。但是如果在多线程环境下会有问题,可以在getInstance()方法上添加synchronized解决;虽然能解决多线程环境下的问题,但是会有性能问题,因为只有在多线程下首次创建单例时才需要提供线程安全保护,创建完成之后就不需要synchronized了,此时的优化就是DCL懒汉式(DOUBLE CHECK LOCKING 双检锁懒汉式),详见方式4.

  1. 懒汉式单例 - DCL(DOUBLE CHECK LOCKING) 双检锁
public class Singleton4 implements Serializable {
    private Singleton4() {
        System.out.println("private Singleton4()");
    }

    /**必须加 volatile 的修饰(解决共享变量的 可见性,有序性)在这里主要是有序性问题,禁止指令重排序
     * 需要理解 INSTANCE = new Singleton4(); 底层工作过程
     * 原因:
     * INSTANCE = new Singleton4(); 这行代码 对应 反编译之后代码如下
     * 创建对象,分配内存空间;(计算有哪些成员变量,需要的内存空间)
     * 17: new           #6                  // class com/cls/demo/基础篇/pattern/Singleton4
     * 20: dup
     * 调用构造方法  <init> 指构造方法 (构造方法和创建对象是两步操作) 成员变量的赋值 是在 构造方法中执行的
     * 21: invokespecial #7                  // Method "<init>":()V
     * 给静态变量赋值 在这里是 将创建好的对象赋值给 INSTANCE
     * 24: putstatic     #5                  // Field INSTANCE:Lcom/cls/demo/基础篇/pattern/Singleton4;
     * 即三步,创建对象,调用构造,给静态变量赋值
     * 但是cpu可能会对指令的执行次序做出优化,如果指令之间没有因果关系(谁先谁后没关系),cpu可能会调换他们的执行次序
     * 上述反编译之后的  21,24是可能被调换次序的,在单线程下没有影响,在多线程下可能会有问题
     *
     */
    private static volatile Singleton4 INSTANCE = null;

    public static Singleton4 getInstance() {
        /**
         * 加锁之前先判断是否为空
         */
        if (INSTANCE == null) {
            synchronized (Singleton4.class) {
                //防止两个线程都进入了上层的if的情况
                if (INSTANCE == null) {
                    INSTANCE = new Singleton4();
                }
            }
        }
        return INSTANCE;
    }

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

INSTANCE = new Singleton4() 不是一个原子操作,分成 3 步:创建对象、调用构造、给静态变量赋值,其中后两步可能被指令重排序优化,变成先赋值、再调用构造,如果线程1 先执行了赋值,线程2 执行到第一个 INSTANCE == null 时发现 INSTANCE 已经不为 null,此时就会返回一个未完全构造的对象
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YlfjE3YG-1658052875084)(en-resource://database/1570:1)]

  1. 懒汉式单例 - 静态内部类 (比较推荐的懒汉式单例的实现)
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()");
    }
}
public class TestSingleton {
    public static void main(String[] args) throws Exception {
        //Singleton1 即使提到最前面也会报错?new Singleton1();执行时间  调反射会发生类加载,就会实例化INSTANCE对象
        //reflection(Singleton1.class);

        //测试是懒汉式还是饿汉式,在调用getInstance()之前,调用另一个静态方法,触发当前这个类的加载链接初始化,就会导致这个单例对象被创建
        System.out.println("otherMethod之前。。");
        Singleton5.otherMethod();

        System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
        System.out.println(Singleton3.getInstance());
        System.out.println(Singleton3.getInstance());

        // 反射破坏单例   就不是单例了
        /**Singleton1 如果移到最上面还是会报错的?反射创建实例必会导致初始化的呀,初始化会把static那里执行完的?
         * 先会执行反射创建对象,然后创建完成之后优惠调用 //提供静态的成员变量  private static final Singleton1 INSTANCE = new Singleton1(); 则会报错
         */
        //reflection(Singleton3.class);

        // 反序列化破坏单例(需要实现Serializable接口)
        serializable(Singleton3.getInstance());

        // Unsafe(jdk内置不能直接访问,可以反射拿到Unsafe实例) 破坏单例(预防不了)
        unsafe(Singleton3.class);
    }

    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 会对枚举类做特殊处理,遇到枚举类会直接将枚举类实例直接返回
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
        System.out.println("反序列化创建实例:" + ois.readObject());//字节流还原为对象 会产生新对象而且是不走构造方法的;写了readResolve()就会调用readResolve()返回对象,就不会使用字节数组反序列化的结果了
    }

    private static void reflection(Class<?> clazz) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
        System.out.println("反射。。");
        //非枚举时使用
        Constructor<?> constructor = clazz.getDeclaredConstructor();

        /**枚举是没有无参构造的 所以枚举时调用 getDeclaredConstructor() 会有问题
         * 枚举构造是有两个参数的(可以看Sex类)
         */
        //枚举时使用  也是会报错的 Cannot reflectively create enum objects
        //Constructor<?> constructor = clazz.getDeclaredConstructor(String.class,int.class);

        constructor.setAccessible(true);
        System.out.println("反射创建实例:" + constructor.newInstance());

        //枚举时
        //System.out.println("反射创建实例:" + constructor.newInstance("OTHER",1));
    }
}

在此例中(1)饿汉、(2)枚举方式以及(5)静态内部类实现的单例实例化都处于类加载时机,所以它们都是线程安全的;(3)不是线程安全的,(4)是线程安全的(DCL)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值