设计模式-单例模式练习

简介: 单例模式是Java设计模式中的重要概念,确保一个类只有一个实例并提供全局访问点。本文详解单例模式的核心思想、实现方式及线程安全问题,包括基础实现(双重检查锁)、懒汉式与饿汉式对比,以及枚举实现的优势。通过代码示例和类图,深入探讨不同场景下的单例应用,如线程安全、防止反射攻击和序列化破坏等,展示枚举实现的简洁与可靠性。

在学习 Java 设计模式时,单例模式(Singleton Pattern) 是一个非常重要的模式,它确保一个类在整个应用程序运行期间只会有一个实例,并提供一个全局的访问点。本文将深入探讨单例模式的核心思想、不同实现方式、线程安全问题及其解决方案。

练习 1:基础单例模式实现

题目:请实现一个经典的单例模式,确保在多线程环境下也是安全的。要求:该单例类有一个方法 getInstance,可以返回单例的实例对象。

提示:可以使用双重检查锁(Double-Checked Locking)来确保线程安全。

代码示例:

package org.example;

public class Singleton02 {
   
    // 构造方法是私有的! 只有在类的内部才能访问,这意味着外部的类不可能通过 new 创建该类的实例。
    // 这里用到了一个经典的小技巧:如果已经有实例了,就拒绝再次创建,避免意外的重复实例化。
    private Singleton02() {
   
        // 如果实例已经存在,扔出一个异常,提示你乖乖使用 getInstance() 方法。
        // 不要直接用 new,这可是单例模式的大忌哦!
        if (instance != null) {
   
            throw new RuntimeException("Use getInstance() method to get the single instance of this class.");
        }
    }

    // 用 volatile 关键字修饰,确保多线程环境下的可见性和禁止指令重排。
    private static volatile Singleton02 instance;

    // 提供一个公共的静态方法,用来获取类的唯一实例。
    public static Singleton02 getInstance() {
   
        // 第一个 if 是一个快速检查——如果已经有实例,直接返回,节省时间!
        if (instance == null) {
   
            // 为了避免多线程的同时访问,使用 synchronized 锁住类对象。
            synchronized (Singleton02.class) {
   
                // 再次检查,如果还是 null,才创建实例!这是为了防止多个线程争抢锁之前有其他线程已经创建了实例。
                if (instance == null) {
   
                    instance = new Singleton02(); // 就这一次,创造唯一的实例!
                }
            }
        }
        return instance; // 返回那唯一的、尊贵的实例对象!
    }
}

测试:

import org.example.Singleton02;
import org.junit.Test;

public class test {
   
    @Test
    public void getSingletontest() {
   
        for (int i = 0; i < 10; i++) {
   
            new Thread(() -> {
   
                Singleton02 singleton02 = Singleton02.getInstance();
                System.out.println(singleton02);
            }).start();
        }

    }
}

image.png
类图:

classDiagram
    class Client {
        +main()
    }
    class Singleton02 {
        -Singleton02 instance$
        -Singleton02()
        +getInstance()$ Singleton02
    }
    Client -- Singleton02 : uses

单例模式实现的基本步骤

  • 私有化构造方法,防止外部通过new操作创建实例;
  • 提供一个静态私有变量来保存类的唯一实例;
  • 提供一个静态共有方法用于获取唯一的实例对象。

我们为什么要使用volatile关键字?

在这里,我们使用了volatile关键字来保证可见性以及防止jvm指令重排。

Singleton02 类的双重检查锁定的实现中,instance = new Singleton02();并不是一个原子操作,这个操作实际上涉及了多个操作,在这一过程,jvm至少做了以下3件事:

  • 第一步给Singleton02对象分配内存空间;
  • 第二步开始调用Singleton02的构造方法,初始化对象的属性;
  • 第三步,将分配的内存地址赋值给instance变量。
    如果我们没有使用volatile关键字的话,jvm可能会发生指令重排序,使第二步第三步的顺序发生变化,如果这时有某个线程在instance引用被赋值之后,它的对象还并没有被初始化的时候访问了instance,这个时候就可能会发生程序报错。

为什么要进行第二次检查?

  • 第一次检查,如果instance已经创建实例,则直接返回;
  • 第二次检查,是为了防止多个线程争抢锁之前有其他线程已经创建了实例。

练习 2:懒汉式单例和饿汉式单例的比较

题目:请分别实现“懒汉式单例”和“饿汉式单例”。然后写一个简单的测试类来验证它们的区别(例如,创建对象的时机和多线程环境下的表现)。

提示:饿汉式在类加载时就会创建实例,懒汉式在需要时才创建实例。

代码示例:

public class SingletonLazy {
    // 懒汉式单例
    // 私有静态实例,延迟加载 
    private static SingletonLazy instance;
    private SingletonLazy() {
    // 私有构造方法,防止外部实例化
        if (instance != null) {
   
            throw new RuntimeException("Use getInstance() method to get the single instance of this class."); } } // 防止反射攻击 
    public static synchronized SingletonLazy getInstance() {
   // 公有的静态方法,通过同步来控制多线程访问
        if (instance == null) {
   
            instance = new SingletonLazy();
        } return instance;
    }
}
public class SingletonHungry {
    // 饿汉式单例
    // 在类加载时创建实例,静态成员变量
    private static final SingletonHungry instance = new SingletonHungry();

    // 私有构造方法,防止外部实例化
    private SingletonHungry() {
   
        // 防止反射攻击
        if (instance != null) {
   
            throw new RuntimeException("Use getInstance() method to get the single instance of this class.");
        }
    }

    // 公有的静态方法,直接返回实例
    public static SingletonHungry getInstance() {
   
        return instance;
    }
}

类图:

classDiagram
    class Client {
        +main()
    }
    class SingletonLazy {
        -SingletonLazy instance$
        -SingletonLazy()
        +getInstance()$ SingletonLazy
    }
    class SingletonHungry {
        -SingletonHungry instance$
        -SingletonHungry()
        +getInstance()$ SingletonHungry
    }
    Client -- SingletonLazy : uses >
    Client -- SingletonHungry : uses >

懒汉式与饿汉式单例模式的比较

特性 饿汉式单例模式 懒汉式单例模式
实例化时机 在类加载时实例化 在第一次调用 getInstance() 时实例化
线程安全性 线程安全(类加载时创建实例,JVM 保证类加载过程是线程安全的) 需要在 getInstance() 方法中使用同步机制保证线程安全
资源消耗 类加载时就会创建实例,占用一定内存 只有在需要时才创建实例,节省资源
实现难度 实现简单 需要使用同步,且可能涉及双重检查锁和 volatile 关键字
适用场景 适用于类加载就需要实例化的场景 适用于实例化比较消耗资源且在调用时才需要的场景

练习 3:枚举单例

题目:使用 Java 的 enum 枚举来实现一个单例模式。并创建测试类,验证其单例性。

提示:枚举类型是创建单例的最简洁和高效的方法之一,它可以防止反射攻击和序列化攻击。

public enum Singleton_06 {
   
    INSTANCE; // 定义唯一的实例

    private Object data; // 单例的属性

    // 获取属性的方法
    public Object getData() {
   
        return data;
    }

    // 设置属性的方法
    public void setData(Object data) {
   
        this.data = data;
    }

    // 获取实例的静态方法
    public static Singleton_06 getInstance() {
   
        return INSTANCE;
    }
}

类图:

classDiagram
    class Client {
        +main()
    }
    class Singleton_06 {
        <<enumeration>>
        INSTANCE$
        -Object data
        +getData() Object
        +setData(Object) void
        +getInstance()$ Singleton_06
    }
    Client -- Singleton_06 : uses >

实现要点:

  • 唯一的实例 INSTANCE:在 enum 中定义了一个唯一的实例 INSTANCE,这是 enum 实现单例模式的核心。在类加载时,Java 虚拟机(JVM)会自动创建该唯一实例,并在整个应用程序生命周期中保持唯一。即使在多线程环境下,也能确保这个实例的唯一性。
  • 属性和方法:在枚举类 Singleton_06 中定义了属性 data,并通过 setData()getData() 方法来访问和修改它。这样的设计可以让 INSTANCE 存储某些全局共享的数据或状态,而不仅仅是一个简单的实例。这样我们不仅拥有单例模式的所有特性,还可以通过枚举实例来管理共享的数据或行为。
  • 静态方法 getInstance():虽然直接通过 Singleton_06.INSTANCE 访问实例已经非常直观,但为了提高代码的可读性和一致性,提供了一个静态方法 getInstance() 来返回这个唯一实例。这样不仅让代码更清晰,也遵循了我们常见的单例模式命名约定。

enum 实现单例的优势

  • 线程安全性enum 类型的线程安全性由 JVM 自动保证。每个 enum 类型在类加载时只会被初始化一次,由 JVM 确保在多线程环境下不会有竞态条件。这比手动编写双重检查锁等实现方式更加简单和可靠。
  • 防止反射攻击:传统的单例实现方式可能会通过反射破坏,因为反射可以访问私有构造方法。然而,enum 在设计时通过特殊机制阻止了反射创建新的实例。当你尝试通过反射调用 enum 的私有构造方法时,JVM 会抛出 IllegalArgumentException,有效地防止了反射攻击。
  • 防止序列化攻击:在序列化和反序列化过程中,普通的单例模式可能会因为反序列化而创建新的实例。而 enum 类型在序列化时由 JVM 维护唯一性,无需手动重写 readResolve() 方法来防止反序列化破坏单例。这大大简化了代码逻辑,同时提高了可靠性。
  • 代码简洁性:相比传统的单例模式实现(如懒汉式、饿汉式或双重检查锁定),enum 方式更为简洁明了。使用 enum 省去了手动编写同步控制和双重检查等复杂代码,同时还解决了反射和序列化带来的潜在问题。其实现代码通常一到两行,显得简洁而优雅。

使用 enum 来实现单例模式是一种被广泛推崇的方式,因其代码简单、自然,并且由 JVM 提供了天然的安全性。与传统的单例模式实现相比,enum 更能有效避免反射和序列化攻击等棘手问题,使得代码更加健壮。通过这种方式,我们可以轻松定义一个安全且优雅的单例。

今天先不学了!我要吃晚饭!!!

0CA81741.gif

目录
相关文章
|
16天前
|
设计模式 缓存 安全
【设计模式】【创建型模式】单例模式(Singleton)
一、入门 什么是单例模式? 单例模式是一种设计模式,确保一个类只有一个实例,并提供一个全局访问点。它常用于需要全局唯一对象的场景,如配置管理、连接池等。 为什么要单例模式? 节省资源 场景:某些对象创
72 15
|
7月前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
58 2
|
2月前
|
设计模式 安全 Java
设计模式:单例模式
单例模式是一种创建型设计模式,确保一个类只有一个实例,并提供全局访问点。它通过私有化构造函数、自行创建实例和静态方法(如`getInstance()`)实现。适用于数据库连接池、日志管理器等需要全局唯一对象的场景。常见的实现方式包括饿汉式、懒汉式、双重检查锁、静态内部类和枚举。线程安全问题可通过`synchronized`或双重检查锁解决,同时需防止反射和序列化破坏单例。优点是避免资源浪费,缺点是可能增加代码耦合度和测试难度。实际开发中应优先选择枚举或静态内部类,避免滥用单例,并结合依赖注入框架优化使用。
|
3月前
|
设计模式 存储 安全
设计模式2:单例模式
单例模式是一种创建型模式,确保一个类只有一个实例,并提供全局访问点。分为懒汉式和饿汉式: - **懒汉式**:延迟加载,首次调用时创建实例,线程安全通过双重检查锁(double check locking)实现,使用`volatile`防止指令重排序。 - **饿汉式**:类加载时即创建实例,线程安全但可能浪费内存。 示例代码展示了如何使用Java实现这两种模式。
40 4
|
5月前
|
设计模式 存储 前端开发
前端必须掌握的设计模式——单例模式
单例模式是一种简单的创建型设计模式,确保一个类只有一个实例,并提供一个全局访问点。适用于窗口对象、登录弹窗等场景,优点包括易于维护、访问和低消耗,但也有安全隐患、可能形成巨石对象及扩展性差等缺点。文中展示了JavaScript和TypeScript的实现方法。
139 13
|
5月前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
61 2
|
6月前
|
设计模式 Java 数据库连接
Java编程中的设计模式:单例模式的深度剖析
【10月更文挑战第41天】本文深入探讨了Java中广泛使用的单例设计模式,旨在通过简明扼要的语言和实际示例,帮助读者理解其核心原理和应用。文章将介绍单例模式的重要性、实现方式以及在实际应用中如何优雅地处理多线程问题。
87 4
|
6月前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
|
6月前
|
设计模式 存储 数据库连接
PHP中的设计模式:单例模式的深入理解与应用
【10月更文挑战第22天】 在软件开发中,设计模式是解决特定问题的通用解决方案。本文将通过通俗易懂的语言和实例,深入探讨PHP中单例模式的概念、实现方法及其在实际开发中的应用,帮助读者更好地理解和运用这一重要的设计模式。
59 1
|
6月前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式