手撕单例模式

1.单例模式的核心目标:确保一个类在程序的整个生命周期中,有且只有一个实例,并提供一个全局访问点来获取这个实例。单例模式的本质就是只能自己创建对象,且只能创建一个对象(确保全局唯一实例)。

2.单例模式的应用场景:当多个模块需要共享同一个资源,或者创建某个对象的成本很高的时候,适合使用单例模式。

(1)日志记录器:整个日志系统使用同一个日志对象来记录信息,避免文件被多个对象同时写入造成错乱。

(2)数据库连接池:全局共享一个连接池对象,复用数据库连接,节省资源。

(3)配置信息管理:在程序启动时读取配置文件,之后所有模块都通过一个对象访问配置。

(4)硬件接口:比如打印机的后台处理程序,防止多个任务同时操作打印机。

3.实现单例模式的关键:

(1)私有化构造方法:防止外部通过new关键字创建对象。

(2)单例模式中,构造器是私有的,不允许外部创建对象,所以必须提供一个不依赖对象的访问方式。静态成员(静态变量和静态方法)不属于任何一个对象实例,而是属于类本身,它们在内存中有一块独立的存储区域,不依附于任何对象而存在。只有静态变量+静态方法,才能共同实现"不创建对象就能访问对象"。

(a)提供一个静态变量:在类的内部持有自己唯一的实例。

(b)提供一个公开的静态方法:作为全局访问点,让外部获取这个内部持有的唯一实例。

4.常见的实现方式:

(1)懒汉式:延迟加载,按需创建。也叫双重检查锁模式。在第一次调用时才会创建实例,在多线程环境下需要加锁保证线程安全。

(2)饿汉式:最简单、最常用。在类加载时就完成实例的创建,是线程安全的。缺点是如果这个类一直没被使用,会提取占用内存。

附代码:

一、懒汉式:双重检查锁模式。

java 复制代码
// 懒汉式单例(双重检查锁定)
class SingletonLazy {
    // 懒汉式的双重检查锁模式将对象的创建延迟到了运行时的首次调用getInstance()时,破坏了JVM的天然保障
    // 因此必须手动使用volatile来禁止指令的重排,防止其他线程看到未初始化的对象
    // 也就是说懒汉式的线程安全保障需要依赖于外界的volatile + synchronized双重机制
    private static volatile SingletonLazy instance = null;

    // 私有构造方法(函数)
    private SingletonLazy() {}

    // 双重检查锁定
    public static SingletonLazy getInstance() {
        if (instance == null) {
            synchronized (SingletonLazy.class) {
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }

    public void showMessage() {
        System.out.println("Lazy Singleton: Hello World!");
    }
}

二、饿汉式:在类加载时就完成实例的创建,是线程安全的。

java 复制代码
// 饿汉式单例
class SingletonHungry {
    // 饿汉式是在类加载阶段创建对象实例,本身就是线程安全的,不需要使用volatile
    private static final SingletonHungry instance = new SingletonHungry();
    
    // 私有构造方法,防止外部实例化
    private SingletonHungry() {}
    
    // 提供全局访问点
    public static SingletonHungry getInstance() {
        return instance;
    }
    
    public void showMessage() {
        System.out.println("Hungry Singleton: Hello World!");
    }
}

ACM模式:

一、懒汉式:双重检查锁模式。

java 复制代码
import java.util.Scanner;

// 懒汉式单例(双重检查锁定)
class SingletonLazy {
    // 懒汉式的双重检查锁模式将对象的创建延迟到了运行时的首次调用getInstance()时,破坏了JVM的天然保障
    // 因此必须手动使用volatile来禁止指令的重排,防止其他线程看到未初始化的对象
    // 也就是说懒汉式的线程安全保障需要依赖于外界的volatile + synchronized双重机制
    private static volatile SingletonLazy instance = null;
    
    // 私有构造方法
    private SingletonLazy() {}
    
    // 双重检查锁定
    public static SingletonLazy getInstance() {
        if (instance == null) {
            synchronized (SingletonLazy.class) {
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
    
    public void showMessage() {
        System.out.println("Lazy Singleton: Hello World!");
    }
}

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        
        int n = scanner.nextInt();
        
        for (int i = 0; i < n; i++) {
            // 创建了两个对象引用(变量),在单例模式中,这两个引用指向的是同一个对象实例
            // 如果不是单例模式且每次getInstance()都new对象,那么在绝大多数情况下每次都new的是新对象
            // getInstance是静态方法,所以要用类名调用
            SingletonLazy singleton1 = SingletonLazy.getInstance();
            SingletonLazy singleton2 = SingletonLazy.getInstance();
            
            // 由于这里的SingletonLazy是一个类,因此singleton1和singleton2都是对象引用
            // 因此 == 比较的是两个引用指向的内存地址是否相等(是否指向了堆内存中的同一个对象实例)
            System.out.println(singleton1 == singleton2);
            // showMessage是实例方法,所以要用对象调用
            singleton1.showMessage();
        }
        
        scanner.close();
    }
}

二、饿汉式:在类加载时就完成实例的创建,是线程安全的。

java 复制代码
import java.util.Scanner;

// 饿汉式单例
class SingletonHungry {
    // 饿汉式是在类加载阶段创建对象实例,本身就是线程安全的,不需要使用volatile
    private static final SingletonHungry instance = new SingletonHungry();
    
    // 私有构造方法,防止外部实例化
    private SingletonHungry() {}
    
    // 提供全局访问点
    public static SingletonHungry getInstance() {
        return instance;
    }
    
    public void showMessage() {
        System.out.println("Hungry Singleton: Hello World!");
    }
}

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        
        // 读取操作次数
        int n = scanner.nextInt();
        
        for (int i = 0; i < n; i++) {
            // 创建了两个对象引用(变量),在单例模式中,这两个引用指向的是同一个对象实例
            // 如果不是单例模式且每次getInstance()都new对象,那么在绝大多数情况下每次都new的是新对象
            // getInstance是静态方法,所以要用类名调用
            SingletonHungry singleton1 = SingletonHungry.getInstance();
            SingletonHungry singleton2 = SingletonHungry.getInstance();
            
            // 由于这里的SingletonHungry是一个类,因此singleton1和singleton2都是对象引用
            // 因此 == 比较的是两个引用指向的内存地址是否相等(是否指向了堆内存中的同一个对象实例)
            System.out.println(singleton1 == singleton2);
            
            // showMessage是实例方法,所以要用对象调用
            singleton1.showMessage();
        }
        
        scanner.close();
    }
}
相关推荐
星空ξ1 小时前
OpenCode + Oh-My-OpenCode 配置指南:集成 GitHub Copilot 模型与 Java LSP (jdtls)
java·github·copilot·opencode·oh-my-opencode
Seven971 小时前
Tomcat Request请求处理:Container设计
java
逸Y 仙X1 小时前
文章十五:ElasticSearch 运用ingest加工索引数据
java·大数据·elasticsearch·搜索引擎·全文检索
70asunflower1 小时前
堆与栈:C 语言内存管理的核心概念
c语言·开发语言
wjs20241 小时前
Rust 输出到命令行
开发语言
xingpanvip1 小时前
星盘接口开发文档:日返比接口指南
开发语言·lua
初心未改HD1 小时前
Go语言Goroutine与Channel深度解析
开发语言·golang
京师20万禁军教头1 小时前
35面向对象(中级)-编程思想
java
SilentSamsara2 小时前
Python 并发基础:threading/GIL 与 multiprocessing 的选型逻辑
服务器·开发语言·数据库·vscode·python·pycharm