重学设计模式 | 单例模式(Singleton Pattern)(中)

简介: 重学设计模式 | 单例模式(Singleton Pattern)

运行后,程序只输出了一个flag = true,然后就死循环卡住了,不会输出:flag为true!原因是:


我们在子线程中修改了flag的值,但是主线程并不知道这个更改,使用的依旧是之前的旧值,所以会一直死循环。


而只要我们为flag添加volatile修饰,程序就能正常结束了:


网络异常,图片无法展示
|


除此之外为if(test.ifFlag())加上synchronized锁也可以解决可见性问题~


线程在进入synchronized代码块前后,会获得锁,清空工作内存,从主内存拷贝共享变量最新的值到工作内存称为副本,执行代码,将修改后的副本的值刷回主内存中,最后线程释放锁。


最后一点,volatile无法保证原子性(一次操作,要么完全成功,要么完全失败),比如下面的代码示例:


public class VolatileTest {
    public static volatile int count = 0;
    public static void increase() {
        count++;
    }
    public static void main(String[] args) {
        Thread[] threads = new Thread[20];
        for(int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(() -> {
                for(int j = 0; j < 1000; j++) {
                    increase();
                }
            });
            threads[i].start();
        }
        // 等待所有累加线程结束,此处>2的原因是idea执行用户代码时会创建一个监控线程Monitor
        // 可以调用 Thread.currentThread().getThreadGroup().list() 查看一番~
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(count);
    }
}


创建了20个线程,每个线程对变量count进行1000此自增,并发结果正常应该是20000,但实际运行过程中结果很多时候都不够20000,原因是count++这个自增操作不是院子操作。解决方法也很简单,要么加锁,要么使用原子类,如:AtomicInteger。


总结下就是:


volatile是JVM提供的一种最轻量级的同步机制,可看做轻量版的synchronized,但不保证原子性,如果是对共享变量进行多个线程的赋值而没有其他操作,那么可以用volatile来代替synchronized。


⑤ 静态内部类(懒加载,线程安全,非常推荐)


public class Singleton {
    private Singleton() { }
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
}


与饿汉式类似,两者都是通过类加载机制来保证初始化instance时只有一个线程,从而避免线程安全问题。


不同之处是Singleton类被加载时,不会立即初始化,只有调用getInstance()函数时,才会装载SingletonHolder类,从而实例化instance,间接实现了懒加载。


0x3、单例的其他安全问题


上述的单例写法都是围绕着 线程安全问题 进行的,即限制了new创建对象,而Java中除了这种创建对象的方式外,还有三种 克隆、反射和序列化,下面演示下如何通过这三种方式破坏单例。


① 克隆破坏单例


clone()是Object自带函数,每个对象都有,直接调用下clone函数,就能创建一个新对象了,那不就把单例破坏了吗?


答:想太多,被克隆类要实现 Cloneable 接口,然后重写clone()函数,才能完成对象克隆,而一般我们的单例是不会实现这个接口的,所以不存在此问题。


② 反射破坏单例


以静态内部类实现的单例为例,我们通过下述代码构建了两个对象,以此破坏单例:


public class ReflectTest {
    public static void main(String[] args) {
        try {
            Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
            constructor.setAccessible(true);    // 禁用访问安全检查
            Singleton s1 = constructor.newInstance();
            Singleton s2 = constructor.newInstance();
            System.out.println(s1.equals(s2)); // 输出结果:false
        } catch (NoSuchMethodException | IllegalAccessException |
                InstantiationException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}


一个最简单的解决方式就是添加一个标志位,当二次调用构造函数时抛出异常,示例如下:


public class Singleton {
    private static boolean flag = true;
    private Singleton() {
        if (flag) {
            flag = !flag;
        } else {
            throw new RuntimeException("有不法之徒想创建第二个实例");
        }
    }
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
}


此时再运行反射代码:


网络异常,图片无法展示
|


Tips:当然先通过反射修改你的flag,在反射调构造方法依旧是可以破坏的~


③ 序列化破坏单例


同样以静态内部类实现的单例为例,先序列化到文件,然后在反序列化恢复为Java对象:


public class SingletonTest {
    public static void main(String[] args) {
        Singleton singleton1 = Singleton.getInstance();
        Singleton singleton2 = null;
        // 序列化
        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
            oos.writeObject(singleton1);
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 反序列化
        try {
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton_file"));
            try {
                singleton2 = (Singleton) ois.readObject();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println(singleton1 == singleton2);   // 输出:false
    }
}


输出false,单例再次被破坏,接着我们来看下这个新对象是怎么创建出来的,从 readObject 跟到 readOrdinaryObject,定位到下述代码:


网络异常,图片无法展示
|


  • isInstantiable():一个serializable/externalizable的类是否可以在运行时被实例化;


  • desc.newInstance():通过反射的方式调用无参构造函数创建一个新对象;


这就是反序列化破坏单例的原理,接着说下怎么规避,在创建新对象的代码处往下走一些:


网络异常,图片无法展示
|


  • desc.hasReadResolveMethod():判断类是否实现了readResolve()函数;


  • desc.invokeReadResolve(obj):有的反射调用此函数,如果在此函数中返回实例就可以了;


修改后的单例类代码:


import java.io.Serializable;
public class Singleton implements Serializable {
    private Singleton() { }
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    private Object readResolve() {
        return getInstance();
    }
}


此时再运行反序列单例时的代码,会输出:true,即同一个对象。


0x4、枚举单例(安全简单,没有懒加载,最佳实践)


上面讲解了除线程安全问题外,三种破坏单例的方式及解决方式,其实用枚举实现单例就能规避这些问题。一个简单的枚举单例代码示例如下:


public enum SingletonEnum {
    INSTANCE;
    private final AtomicLong id = new AtomicLong(0);
    public long getId() {
        return id.incrementAndGet();
    }
}
// 调用
SingletonEnum.INSTANCE.getId()


得益于jdk的enum语法糖,这么简单的代码就能预防这四种问题,接下来一一看下原理。


相关文章
|
8月前
|
设计模式 缓存 安全
【设计模式】【创建型模式】单例模式(Singleton)
一、入门 什么是单例模式? 单例模式是一种设计模式,确保一个类只有一个实例,并提供一个全局访问点。它常用于需要全局唯一对象的场景,如配置管理、连接池等。 为什么要单例模式? 节省资源 场景:某些对象创
313 15
|
10月前
|
设计模式 Java 数据安全/隐私保护
Java 设计模式:装饰者模式(Decorator Pattern)
装饰者模式属于结构型设计模式,允许通过动态包装对象的方式为对象添加新功能,提供比继承更灵活的扩展方式。该模式通过组合替代继承,遵循开闭原则(对扩展开放,对修改关闭)。
|
10月前
|
设计模式 安全 Java
设计模式:单例模式
单例模式是一种创建型设计模式,确保一个类只有一个实例,并提供全局访问点。它通过私有化构造函数、自行创建实例和静态方法(如`getInstance()`)实现。适用于数据库连接池、日志管理器等需要全局唯一对象的场景。常见的实现方式包括饿汉式、懒汉式、双重检查锁、静态内部类和枚举。线程安全问题可通过`synchronized`或双重检查锁解决,同时需防止反射和序列化破坏单例。优点是避免资源浪费,缺点是可能增加代码耦合度和测试难度。实际开发中应优先选择枚举或静态内部类,避免滥用单例,并结合依赖注入框架优化使用。
|
9月前
|
设计模式 存储 安全
设计模式-单例模式练习
单例模式是Java设计模式中的重要概念,确保一个类只有一个实例并提供全局访问点。本文详解单例模式的核心思想、实现方式及线程安全问题,包括基础实现(双重检查锁)、懒汉式与饿汉式对比,以及枚举实现的优势。通过代码示例和类图,深入探讨不同场景下的单例应用,如线程安全、防止反射攻击和序列化破坏等,展示枚举实现的简洁与可靠性。
165 0
|
11月前
|
设计模式 存储 安全
设计模式2:单例模式
单例模式是一种创建型模式,确保一个类只有一个实例,并提供全局访问点。分为懒汉式和饿汉式: - **懒汉式**:延迟加载,首次调用时创建实例,线程安全通过双重检查锁(double check locking)实现,使用`volatile`防止指令重排序。 - **饿汉式**:类加载时即创建实例,线程安全但可能浪费内存。 示例代码展示了如何使用Java实现这两种模式。
261 4
|
设计模式 存储 前端开发
前端必须掌握的设计模式——单例模式
单例模式是一种简单的创建型设计模式,确保一个类只有一个实例,并提供一个全局访问点。适用于窗口对象、登录弹窗等场景,优点包括易于维护、访问和低消耗,但也有安全隐患、可能形成巨石对象及扩展性差等缺点。文中展示了JavaScript和TypeScript的实现方法。
555 13
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
186 2
|
8月前
|
设计模式 Java 数据库连接
【设计模式】【创建型模式】工厂方法模式(Factory Methods)
一、入门 什么是工厂方法模式? 工厂方法模式(Factory Method Pattern)是一种创建型设计模式,它定义了一个用于创建对象的接口,但由子类决定实例化哪个类。工厂方法模式使类的实例化延迟
256 16
|
8月前
|
设计模式 负载均衡 监控
并发设计模式实战系列(2):领导者/追随者模式
🌟 ​大家好,我是摘星!​ 🌟今天为大家带来的是并发设计模式实战系列,第二章领导者/追随者(Leader/Followers)模式,废话不多说直接开始~
248 0
|
8月前
|
设计模式 监控 Java
并发设计模式实战系列(1):半同步/半异步模式
🌟 ​大家好,我是摘星!​ 🌟今天为大家带来的是并发设计模式实战系列,第一章半同步/半异步(Half-Sync/Half-Async)模式,废话不多说直接开始~
240 0