【面试:基础篇09:单例模式全总结】

简介: 【面试:基础篇09:单例模式全总结】

【面试:基础篇09:单例模式全总结】

01.简介

单例模式是指 一个类控制它只有一个实例,实现单例模式有五种方式:饿汉式、枚举饿汉式、懒汉式(无优化)、懒汉式(双检锁优化)、懒汉式(内部类优化)

02.饿汉式单例:

特点:

饿汉式实现单例的一种方式,它的特点是 初始化时 静态成员变量就被单例模式的唯一实例赋值,饿汉式是线程安全的

代码:

class Singleton1 implements Serializable{
    private Singleton1(){
        System.out.println("饿汉式 初始化");
    }
    private static final Singleton1 single = new Singleton1();
    public static Singleton1 getInstance(){
        return single;
    }
    public static void other(){
        System.out.println("饿汉式 调用其他方法也会初始化实例");
    }
}
public class danli {
    public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, IOException, ClassNotFoundException {
        Singleton1.other();// 调用other方法时就已经创建好了单例对象
        System.out.println("==================================");
        System.out.println(Singleton1.getInstance());
        System.out.println(Singleton1.getInstance());
    }
}

结果

可以看出饿汉式 只会创建一个实例,且在调用其他类之前就完成了构造函数初始化

02.1反射破坏单例的代码优化

优化方式:

在构造函数里加一个判断,如果没有创造过单例对象则可以创建 否则抛出异常
class Singleton1 implements Serializable{
    private Singleton1(){
        // 一:无优化时
        /*if (single!=null){ // 避免反射暴力破坏单例模式
            throw new RuntimeException("单例对象不能重复创建");
        }*/
        // 二:优化后
        if (single!=null){ // 避免反射暴力破坏单例模式
            throw new RuntimeException("单例对象不能重复创建");
        }
        System.out.println("饿汉式 初始化");
    }
    private static final Singleton1 single = new Singleton1();
    public static Singleton1 getInstance(){
        return single;
    }
    public static void other(){
        System.out.println("饿汉式 调用其他方法也会初始化实例");
    }
}

public class danli {
    public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, IOException, ClassNotFoundException {
        Singleton1.other();// 调用other方法时就已经创建好了单例对象
        System.out.println("==================================");
        System.out.println(Singleton1.getInstance());
        System.out.println(Singleton1.getInstance());

        // 反射破坏单例
        reflection(Singleton1.class);
    }
    // 反射获取实例代码
    private static void reflection(Class<?> clazz) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Constructor<?> constructor = clazz.getDeclaredConstructor();// 获取构造函数
        constructor.setAccessible(true);// true表示可以使用私有构造
        System.out.println("反射创建实例:"+constructor.newInstance());// 实例又被创建了 破坏单例
    }
}

结果

无优化:

优化:

可以看出无优化时 反射创建了一个不同的实例 成功破坏了单例模式,在我们优化后成功的抛出了异常。

02.2反序列化破坏单例的代码优化

优化方式:

在单例模式中添加readResolve()方法
// 饿汉式
class Singleton1 implements Serializable{
    private Singleton1(){
        System.out.println("饿汉式 初始化");
    }
    private static final Singleton1 single = new Singleton1();
    public static Singleton1 getInstance(){
        return single;
    }
    public static void other(){
        System.out.println("饿汉式 调用其他方法也会初始化实例");
    }
    // 反序列化无优化时
    /*public Object readResolve(){ // 避免反序列化破坏单例模式
        return single;
    }*/

    // 反序列化优化后
    public Object readResolve(){ // 避免反序列化破坏单例模式
        return single;
    }
}
public class danli {
    public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, IOException, ClassNotFoundException {
        Singleton1.other();// 调用other方法时就已经创建好了单例对象
        System.out.println("==================================");
        System.out.println(Singleton1.getInstance());
        System.out.println(Singleton1.getInstance());

        // 反序列化破坏单例
        serializable(Singleton1.getInstance());
    }
    // 反序列化获取实例代码
    private static void serializable(Object instance) throws IOException, ClassNotFoundException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();// 创建字节数组输出流
        ObjectOutputStream oos = new ObjectOutputStream(bos);// 放入ObjectOutputStream管道
        oos.writeObject(instance);// 把instance写入字节流
        // 把字节流重新读取出来 还原了新的对象,不会走构造方法
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
        System.out.println("反序列化创建实例:"+ois.readObject());
    }
}

无优化:

优化后:

可以看出无优化时 反序列化创建了一个不同的实例 成功破坏了单例模式,在我们优化后成功避免了这个问题。

02.3 Unsafe破坏单例的代码(不能优化)

// 饿汉式
class Singleton1 implements Serializable{
    private Singleton1(){
        System.out.println("饿汉式 初始化");
    }
    private static final Singleton1 single = new Singleton1();
    public static Singleton1 getInstance(){
        return single;
    }
    public static void other(){
        System.out.println("饿汉式 调用其他方法也会初始化实例");
    }
}

public class danli {
    public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, IOException, ClassNotFoundException {
        Singleton1.other();// 调用other方法时就已经创建好了单例对象
        System.out.println("==================================");
        System.out.println(Singleton1.getInstance());
        System.out.println(Singleton1.getInstance());

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

    private static void unsafe(Class<?> clazz) throws InstantiationException {
        Object o = UnsafeUtils.getUnsafe().allocateInstance(clazz);// 可以根据类型创建类型实例,不会走构造方法
        System.out.println("Unsafe 创建实例:"+o);
    }
}

结果

可以看出利用unsafe方法也可以另外创建单例实例,且这种方式无法避免。

03枚举类实现饿汉式

介绍:

枚举类是由单例实现的,并且枚举类已经天然防范了 反射破坏单例以及反序列化破坏单例

代码

// 枚举类实现饿汉式
enum Singleton2{
    single;// 唯一的实例,之后的代码可以不要 主要是验证它确实是单例
    Singleton2(){// 构造函数默认是private Singleton2,所以private可以不加
        System.out.println("enum实现饿汉式 初始化");
    }

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

    public static Singleton2 getInstance(){// 可以不提供这个方法 枚举类型默认是public
        return single;
    }

    public static void other(){// 验证是不是饿汉式
        System.out.println("饿汉式 调用其他方法也会初始化实例");
    }
}

public class danli2 {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InstantiationException, NoSuchMethodException, IllegalAccessException, InvocationTargetException {
        Singleton2.other();// 调用other方法时就已经创建好了单例对象
        System.out.println("==================================");
        System.out.println(Singleton2.getInstance());
        System.out.println(Singleton2.getInstance());

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

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

        // Unsafe 破坏单例,无法预防
        unsafe(Singleton2.class);
    }
    // 枚举类不怕反射破坏单例
    private static void reflection(Class<?> clazz) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Constructor<?> constructor = clazz.getDeclaredConstructor(String.class,int.class);// 获取有参构造函数
        constructor.setAccessible(true);// true表示可以使用私有构造
        System.out.println("反射创建实例:"+constructor.newInstance("OTHER",1));
    }

    // 枚举类不怕反序列化破坏单例
    private static void serializable(Object instance) throws IOException, ClassNotFoundException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();// 创建字节数组输出流
        ObjectOutputStream oos = new ObjectOutputStream(bos);// 放入ObjectOutputStream管道
        oos.writeObject(instance);// 把instance写入字节流
        // 把字节流重新读取出来 还原了新的对象,不会走构造方法
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
        System.out.println("反序列化创建实例:"+ois.readObject());
    }

    private static void unsafe(Class<?> clazz) throws InstantiationException {
        Object o = UnsafeUtils.getUnsafe().allocateInstance(clazz);// 可以根据类型创建类型实例,不会走构造方法
        System.out.println("Unsafe 创建实例:"+o);
    }

}

正常情况的结果

可以看出枚举类实现的单例 就是饿汉式的 会在调用类时就加载构造函数,且只有一个单例对象

使用反射破坏单例的结果

可以看出 我们并没有对枚举实现的单例模式 做任何的处理 它依旧可以避免反射破坏单例

使用反序列化破坏单例的结果

可以看出 我们并没有对枚举实现的单例模式 做任何的处理 它依旧可以避免反序列化破坏单例

使用Unsafe方法破坏单例

可以看出 如果使用Unsafe方法依旧可以破坏单例。

04懒汉式单例:

特点

懒汉式单例 是实现单例模式的一种方式,他与饿汉式单例的主要区别在于,饿函数单例是在类加载时 静态成员变量就被单例模式的唯一实例赋值 而 懒汉式则是在调用获取实例对象方法时才创建实例进而赋值,懒汉式是线程不安全的。

代码

public class danli3 {
    public static void main(String[] args) {
        Singleton3.other();
        System.out.println("==================================");
        System.out.println(Singleton3.getInstance());
        System.out.println(Singleton3.getInstance());
    }
}

// 懒汉式单例(线程不安全)
class Singleton3 implements Serializable {
    private Singleton3(){
        System.out.println("懒汉式 初始化");
    }
    private static Singleton3 single = null;
    public static Singleton3 getInstance(){
        if (single==null){// 假如有两个线程同时到这 会创建两个单例对象
            single=new Singleton3();
        }
        return single;
    }
    public static void other(){
        System.out.println("懒汉式 调用其它方法 不会初始化单例对象");
    }
}

结果

可以发现懒汉式单例 初始化时不会 初始化构造函数 且 与不会初始化单例对象,需要在调用getInstance()方法后获取单例对象时才会创建单例对象

懒汉式优化并发问题

问题介绍

假如有两个线程同时运行到getInstance()方法时 判断单例对象是否创建时会出现并发问题 导致有两个单例对象被创建

代码

public class danli3 {
    public static void main(String[] args) {
        Singleton3.other();
        System.out.println("==================================");
        System.out.println(Singleton3.getInstance());
        System.out.println(Singleton3.getInstance());
    }
}

// 懒汉式单例(线程不安全)
class Singleton3 implements Serializable {
    private Singleton3(){
        System.out.println("懒汉式 初始化");
    }
    private static Singleton3 single = null;
    public static synchronized Singleton3 getInstance(){
        if (single==null){// 加锁 实现线程同步
            single=new Singleton3();
        }
        return single;
    }
    public static void other(){
        System.out.println("懒汉式 调用其它方法 不会初始化单例对象");
    }
}

解决方法就是 加锁,每次只能有一个线程进入getInstance() 这样就可以只创建一个实例 解决了并发问题。

05懒汉式单例优化 双检锁:

问题介绍

上一种方法虽然解决了并发问题 但因为我们在getInstance()上加锁 导致我们每次调用getInstance()获取实例时都会进行加锁 严重的降低了效率,所以我们在此基础上再进行优化

优化方式是加双检锁 顾名思义 就是加了两层判断 外层判断优化速度 内存判断解决并发问题

代码

public class danli4 {
    public static void main(String[] args) {
        Singleton4.other();
        System.out.println("==================================");
        System.out.println(Singleton4.getInstance());
        System.out.println(Singleton4.getInstance());
    }
}

// 对上一种懒汉式 多线程加锁的优化:双检锁,外层锁优化速度 内层锁为了保证线程安全
class Singleton4 implements Serializable {
    private Singleton4(){
        System.out.println("懒汉式 初始化");
    }
    // 双检锁必须加volatile修饰,作用是解决共享变量的 可见性 有序性,这里主要是为了保证 有序性
    private static volatile Singleton4 single = null;

    public static Singleton4 getInstance(){
        if (single==null){// 再最外面加了一个判断 为了避免每次获取实例 都得走加锁逻辑
            synchronized (Singleton4.class){
                if (single==null){// 里面判断 为了避免多线程第一次同时创建实例 导致创建多个单例对象
                    single=new Singleton4();
                }
            }
        }
        return single;
    }
    public static void other(){
        System.out.println("懒汉式 调用其它方法 不会初始化单例对象");
    }
}

注意:

这里有一个注意点就是 我们在修饰静态变量时 加了volatile,这里它的作用是 为了保证 程序指令的有序性。

==问题描述:==

single=new Singleton4();这行代码背后的指令主要有三个:

1.创建Singleton4对象

2.初始化构造函数

3.给静态变量single赋值

其中2,3指令在程序运行时是可以互换的,在单线程中也完全正确 但是在多线程中就错了。具体看下图

程序有序性原理图

由上图可以看出如果不加volatile会导致指令的顺序可能改变 从而使得线程2的操作插入在 初始化构造函数前,导致虽然单例对象已经创建但是单例对象需要的一些初始化数据 还没有被构造函数初始化,导致初始化不完整。

06懒汉式单例优化 内部类:

特点

我们看到上述实现懒汉式单例的方法都过于复杂了,复杂的原因是因为并发问题,我们又想为什么饿汉式不用考虑并发问题,其根本原因是 饿汉式是最开始就给静态变量赋值 使其变成静态代码块 在类加载时静态代码块就执行了一次且只执行一次 这就导致肯定不可能出现 并发问题,所以我们的优化思路是 把懒汉式单例 初始化静态变量时也变成静态代码块

代码

/*
为什么饿汉式不需要考虑线程安全,因为饿汉式是最开始就给静态变量赋值,这段代码最终放在静态代码块其线程安全
jvm帮我们做了,因为当类第一次被调用时 执行静态代码块且只执行一次 所以一定线程安全
所以我们现在需要把懒汉式 也放入静态代码块就好了
*/

public class danli5 {
    public static void main(String[] args) {
        Singleton5.other();
        System.out.println("==================================");
        System.out.println(Singleton5.getInstance());
        System.out.println(Singleton5.getInstance());
    }
}

class Singleton5 implements Serializable{
    private Singleton5(){
        System.out.println("内部类懒汉式 初始化");
    }

    private static class Holder{// 静态内部类,静态变量赋值,实现静态代码块
        static Singleton5 single = new Singleton5();
    }

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

    public static void other(){
        System.out.println("调用其它方法");
    }
}

结果

可以看出完美的实现了 懒汉式单例 且 这种方式不会出现并发问题,是懒汉式里最推荐的方式。

07JDK中哪些地方体现了单例模式

Runtime类

很明显Runtime类是饿汉式单例

System类中的Console类型单例

很明显是静态变量被volatile修饰 说明是优化版懒汉式

补充:Collections类中很多实现都是单例,可以自己分析

目录
相关文章
|
4月前
|
SQL 安全 Java
Android经典面试题之Kotlin中object关键字实现的是什么类型的单例模式?原理是什么?怎么实现双重检验锁单例模式?
Kotlin 单例模式概览 在 Kotlin 中,`object` 关键字轻松实现单例,提供线程安全的“饿汉式”单例。例如: 要延迟初始化,可使用 `companion object` 和 `lazy` 委托: 对于参数化的线程安全单例,结合 `@Volatile` 和 `synchronized`
60 6
|
4月前
|
设计模式 安全 Java
Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
78 1
|
4月前
|
设计模式 安全 Java
Java面试题:什么是单例模式?如何在Java中实现单例模式?
Java面试题:什么是单例模式?如何在Java中实现单例模式?
63 0
|
4月前
|
设计模式 安全 Java
Java面试题:解释单例模式的实现方式及其优缺点,讨论线程安全性的实现。
Java面试题:解释单例模式的实现方式及其优缺点,讨论线程安全性的实现。
34 0
|
4月前
|
设计模式 安全 NoSQL
Java面试题:设计一个线程安全的单例模式,并解释其内存占用和垃圾回收机制;使用生产者消费者模式实现一个并发安全的队列;设计一个支持高并发的分布式锁
Java面试题:设计一个线程安全的单例模式,并解释其内存占用和垃圾回收机制;使用生产者消费者模式实现一个并发安全的队列;设计一个支持高并发的分布式锁
68 0
|
4月前
|
设计模式 安全 Java
Java面试题:如何实现一个线程安全的单例模式,并确保其在高并发环境下的内存管理效率?如何使用CyclicBarrier来实现一个多阶段的数据处理任务,确保所有阶段的数据一致性?
Java面试题:如何实现一个线程安全的单例模式,并确保其在高并发环境下的内存管理效率?如何使用CyclicBarrier来实现一个多阶段的数据处理任务,确保所有阶段的数据一致性?
62 0
|
4月前
|
存储 设计模式 监控
Java面试题:如何在不牺牲性能的前提下,实现一个线程安全的单例模式?如何在生产者-消费者模式中平衡生产和消费的速度?Java内存模型规定了变量在内存中的存储和线程间的交互规则
Java面试题:如何在不牺牲性能的前提下,实现一个线程安全的单例模式?如何在生产者-消费者模式中平衡生产和消费的速度?Java内存模型规定了变量在内存中的存储和线程间的交互规则
48 0
|
4月前
|
设计模式 安全 NoSQL
Java面试题:结合单例模式与Java内存管理,设计一个线程安全的单例类?分析Java多线程工具类ExecutorService与Java并发工具包中的工具类,设计一个Java并发框架的分布式锁实现
Java面试题:结合单例模式与Java内存管理,设计一个线程安全的单例类?分析Java多线程工具类ExecutorService与Java并发工具包中的工具类,设计一个Java并发框架的分布式锁实现
63 0
|
4月前
|
设计模式 存储 缓存
Java面试题:结合单例模式与Java内存模型,设计一个线程安全的单例类?使用内存屏障与Java并发工具类,实现一个高效的并发缓存系统?结合观察者模式与Java并发框架,设计一个可扩展的事件处理系统
Java面试题:结合单例模式与Java内存模型,设计一个线程安全的单例类?使用内存屏障与Java并发工具类,实现一个高效的并发缓存系统?结合观察者模式与Java并发框架,设计一个可扩展的事件处理系统
38 0
|
4月前
|
设计模式 缓存 安全
Java面试题:详解单例模式与内存泄漏?内存模型与volatile关键字的实操?并发工具包与并发框架的应用实例
Java面试题:详解单例模式与内存泄漏?内存模型与volatile关键字的实操?并发工具包与并发框架的应用实例
33 0