设计模式-单例设计模式

前言

单例设计模式是一种常用的创建型 软件设计模式,它保证一个类只有一个实例存在,并提供一个全局访问方法来访问这个实例。单例模式通常被用于需要共享资源的场景,在合适的场景下使用单例模式可以提高系统性能和资源利用率。如数据库连接、线程池等。

在单例模式中,类的构造函数被设置为私有,这样外部类就无法直接实例化该类。 通过一个静态方法或者静态变量来获取类的唯一实例。这个静态方法或者变量控制了实例的创建和访问,确保了只有一个实例存在。

单例模式的主要优点包括:全局唯一性,可以节省系统资源,提高性能,避免了多实例对资源的竞争问题。然而,单例模式也有一些缺点,如可能引入全局状态,不易于扩展和测试。

在实现单例模式时,可以使用多种方式,如饿汉式、懒汉式、双重检查锁等。每种方式都有其适用的场景和特点,需要根据具体需求选择合适的方式。

单例设计模式分类

饿汉式: 类加载的时候就会创建单例实例。

懒汉式: 类加载不会导致该单例对象被创建,而是首次使用该对象时才会创建。

方式1:饿汉式-使用静态成员变量实现单例模式

步骤如下:

  1. 将构造方法私有化
  2. 静态实例化单例对象
  3. 提供静态方法,供外界访问

废话少说,直接上代码

java 复制代码
public class Singleton {
    private Singleton() {}

    private static final Singleton instance = new Singleton();

    public static Singleton getInstance() {
        return Singleton.instance;
    }
}

测试代码

java 复制代码
public class Client {

    public static void main(String[] args) {
        // 创建singleton对象
        Singleton instance = Singleton.getInstance();
        Singleton instance1 = Singleton.getInstance();

        // 判断两个获取到的对象是不是同一个对象
        System.out.println(instance == instance1);
    }
}

我们都知道在Java中对于java对象 ==比较的是对象的引用和没有重写的equals方法是一样的。

说明: 该方式在成员位置声明singleton类型的静态变量,并创建Singleton类的对象instance。instance对象随类的加载而创建。如果该对象足够大的话,而一直没有使用就会造成内存的浪费。

方式2:饿汉式-使用静态代码块实现单例模式

步骤如下:

  1. 构造方法私有化
  2. 声明一个静态的成员变量,但不赋值
  3. 创建一个静态方法,对成员变量进行赋值
  4. 创建一个公共的静态方法用于获取单例的实例化对象

上代码:

java 复制代码
public class Singleton {

    /**私有构造方法*/
    private Singleton() {}

    /**声明单例类型的变量*/
    private static final Singleton instance;

    /**在静态代码块中进行赋值*/
    static {
        instance = new Singleton();
    }

    /**对外提供获取该单例对象的方法*/
    public static Singleton getInstance() {
        return instance;
    }
}

我们知道类的加载顺序为:静态属性 -> 静态方法 -> 静态代码块

测试:

java 复制代码
public class Client {

    public static void main(String[] args) {
        // 获取对象实例
        Singleton instance = Singleton.getInstance();
        Singleton instance1 = Singleton.getInstance();

        // 校验结果
        System.out.println("instance和instance1是否为同一实例:"+ (instance == instance1));
    }
}

结果:

说明: 同上,也会造成内存的浪费,只是实例化的步骤放置到了静态代码块中执行

方式3:懒汉式-获取时创建-存在线程安全问题

上面说到:对于单例模式懒汉式和饿汉式的区别就是,饿汉式在类加载的时候就会创建单例对象,懒汉式只有在获取该单例对象时才会创建实例化的对象,所以我们只需要稍加改造,放在获取单例对象时即可。

上代码:

java 复制代码
public class Singleton {
    /**私有构造方法*/
    private Singleton() {}

    /**声明一个单列类型的变量*/
    private static Singleton instance;

    /**对外提供一个访问方式*/
    public static Singleton getInstance() {
        // 判断对象是否被实例化
        // 如果没有则进行实例化,如果有则直接返回
        if (null == instance) {
            instance = new Singleton();
        }
        return instance;
    }
}

将该代码和上面的饿汉式进行比较:区别仅仅是将实例化对象的步骤放到了获取对象的时候创建,但是这种懒汉式单例设计模式存在线程安全问题。

测试:

java 复制代码
public class Client {

    public static void main(String[] args) {
        // 获取实例对象
        Singleton instance = Singleton.getInstance();
        Singleton instance1 = Singleton.getInstance();

        // 判断两个实例对象是否相等
        System.out.println(instance == instance1);
    }
}

测试结果:

方式4:懒汉式-双重检查锁

在上面的懒汉式获取单例对象的方式中,我们说过可能出现线程安全问题,原因就是在获取实例对象的时候,线程一和线程二都有可能检查到实例对象未被创建,从而去实例化对象。这样线程一和线程二获取到的对象,就是各自创建的实例对象了,所以我们在此基础上进行加锁,但是这里加锁也是有讲究的,不能直接在获取方法上进行加锁,而应该在写操作的时候进行加锁,读操作不加锁。

下面上代码:

java 复制代码
public class Singleton {

    /**
     * 构造方法私有化
     */
    private Singleton() {
    }

    /**
     * 创建一个静态的成员变量,但不赋值
     */
    private static volatile Singleton instance;

    /**
     * 创建一个获取单例对象的方式
     */
    public static Singleton getInstance() {
        // 第一次判断,如果instance不为null,不需要抢占锁,直接返回对象
        if (null == instance) {
            synchronized (Singleton.class) {
                // 第二次进行判断,当方才竞争锁的线程抢占到锁时,再判断是否实例对象已经被创建,防止实例对象被覆盖掉。
                if (null == instance) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

测试类:

java 复制代码
public class Client {

    private static Singleton instance;
    private static Singleton instance1;

    public static void main(String[] args) throws InterruptedException {
        // 获取实例对象
        new Thread(() -> {
            instance = Singleton.getInstance();
        }).start();

        new Thread(() -> {
            instance1 = Singleton.getInstance();
        }).start();
        // 让线程先执行完毕
        Thread.sleep(500);
        // 打印实例对象
        System.out.println(instance);
        System.out.println(instance1);
        System.out.println(instance == instance1);
    }
}

测试结果:

说明: 双重检查锁-顾名思义,两次检查,使用了锁,在获取该实力对象时,如果该对象已经被实例化了那么直接过去该对象。如果该对象没有被实例化,那么先获取锁,再检查该对象有没有被实例化,如果该对象没有被实例化,则进行实例化,这就是双重检查锁的实现原理。

方式5:懒汉式-使用静态内部类的方式

顾名思义:静态则保证类只会被加载一次,从而保证获取到的类是单例的

废话少说,直接上代码:

java 复制代码
public class Singleton {

    /**私有化构造方法*/
    private Singleton() {}

    /**声明一个静态内部类*/
    private static class SingletonHolder {
        // 在内部类中声明并初始化外部类的对象
        private static final Singleton INSTANCE = new Singleton();
    }

    /**提供一个公共的访问方式供外界获取访问静态实例的方法*/
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

测试代码:

java 复制代码
public class Client {

    public static void main(String[] args) {
        // 获取实例对象
        Singleton instance = Singleton.getInstance();
        Singleton instance1 = Singleton.getInstance();

        // 判断两个对象是否为同一个实例对象
        System.out.println(instance == instance1);
    }
}

测试结果:

方式6:饿汉式-使用静态内部类的方式

一般来说,放在最后的一般都是大BOSS,枚举类也是如此。枚举类实现单例模式是极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且 枚举类型是所有单例模式实现中唯一一种不会被破坏的单例实现模式。

直接上代码:

java 复制代码
public enum Singleton {
    INSTANCE;
}

看到这里,你是否想问:哥你没开玩笑吧!这么简单?

我的回答:对就是这么简单,回想一下你往常的对枚举类的使用,是否有一种恍然大悟的感觉呢?

我们还是测试一下:

java 复制代码
public class Client {

    public static void main(String[] args) {
        Singleton instance = Singleton.INSTANCE;
        Singleton instance1 = Singleton.INSTANCE;

        System.out.println(instance == instance1);
    }
}

查看结果:

总结

讲到这里,单例模式的几种创建方式在这里就介绍完毕啦!我们知道了,创建单例设计模式是为了保证一个类只有一个实例存在,并提供一个全局访问方法来访问这个实例。并且单例设计模式有两种类型,一种是饿汉式,一种是懒汉式。例如,当您在开发一个线程池时,可以使用单例模式来确保只有一个线程池实例。这样可以避免创建多个线程池实例而导致的资源浪费和性能下降。

最后,希望本文对您有帮助的话,不妨点个赞,支持一下作者哟!

相关推荐
IT_陈寒2 小时前
闭包陷阱让我加了两天班,JavaScript你真行
前端·人工智能·后端
易协同低代码3 小时前
通达OA核心类库TD类深度解析
后端
Gopher_HBo3 小时前
Go语言学习笔记(十八)Gin处理Session
后端
谭光志4 小时前
工具塞满上下文窗口怎么办?深度拆解 AI Agent Tool Search 按需加载实现原理
前端·后端·ai编程
她说..4 小时前
Java 默认值设置方式
java·开发语言·后端·springboot
foggyprojects4 小时前
从0开始,一句话启动AI DataAgent
后端·数据分析·ai编程
郡杰4 小时前
一些基础和问题解决
后端
陈随易4 小时前
前端项目部署只要30秒
前端·后端·程序员
YIAN4 小时前
从零手写文件读取 MCP 服务:一文吃透 Model Context Protocol 全链路通信原理
前端·后端·mcp
Imchendiana4 小时前
《狂人日记NO.9》— 前后端一把梭,我的全栈实录
前端·后端