设计模式之单例模式


一、单例模式

单例模式属于创建型模式, 是 Java 中最简单的设计模式之一, 它提供了一种创建对象的最佳方式,

这种模式涉及到一个单一的类, 该类负责创建自己的对象, 同事确保只有单个对象被创建, 提供唯一对象的创建方式, 可以直接访问, 不需要实例化该类的对象

注意:

  1. 单例类智能有一个实例
  2. 单例类必须自己创建自己的唯一实例
  3. 单例类笔记给其他所有对象提供这一实例

二、适用场景

  1. 需要生成唯一序列的环境
  2. 需要频繁实例化然后销毁的对象
  3. 创建对象时耗时过多或者耗资源过多, 但是有经常用到的对象
  4. 方便资源互相通信的环境

三、单例模式的优缺点

优点:

  1. 在内存中只有一个对象, 节省内存空间;
  2. 避免频繁的创建销毁对象, 可以提高性能;
  3. 避免对共享资源的多重占用, 简化访问;
  4. 为整个系统提供一个全局访问点.

缺点:

  1. 不适用于变化频繁的对象;
  2. 滥用单例模式带来的一些问题, 如为了节省资源将数据库连接池对象设计为单例, 可能会导致共享连接池对象的程序过多而出现连接池溢出;
  3. 如果实例化的对象长时间不被利用, 系统会认为该独享是垃圾而被回收, 可能导致对象状态的丢失;

四、单例模式的分类

  1. 懒汉式, 线程不安全
  2. 懒汉式, 线程安全
  3. 饿汉式
  4. 双检锁/双重校验锁
  5. 静态内部类
  6. 枚举

五、懒汉式, 线程不安全的

这种方式是最基本的实现方式, 因为没有加锁(synchronized), 所以这种方式是不安全、不支持多线程的, 且方式会懒加载很明显

java 复制代码
public class Singleton1 {
    private static Singleton1 instance;

    private Singleton1() {
    }

    public static Singleton1 getInstance() {
        if(instance == null){
            instance = new Singleton1();
        }
        return instance;
    }

    public static void main(String[] args) {
        System.out.println(Singleton1.getInstance());
        System.out.println(Singleton1.getInstance());
        System.out.println(Singleton1.getInstance());
    }
}

输出:

text 复制代码
com.kino.designpattern.single.Singleton1@4554617c
com.kino.designpattern.single.Singleton1@4554617c
com.kino.designpattern.single.Singleton1@4554617c

六、懒汉式, 线程安全

像不安全的懒汉式一样, 这种方式也具有很好的懒加载, 适用于多线程, 但是效率很低

优点: 第一次调用才会初始化, 避免内存浪费

缺点: 必须加锁才能保证单例, 加锁会影响性能

java 复制代码
public class Singleton2 {
    private static Singleton2 singleton2;

    private Singleton2() {
    }

    public synchronized static Singleton2 getSingleton2() {
        if (singleton2 == null) {
            singleton2 = new Singleton2();
        }
        return singleton2;
    }

    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable, "singleton2 thread");
        thread.start();
    }
}

输出:

text 复制代码
com.kino.designpattern.single.Singleton2@152ee4dc
com.kino.designpattern.single.Singleton2@152ee4dc
com.kino.designpattern.single.Singleton2@152ee4dc
com.kino.designpattern.single.Singleton2@152ee4dc

七、饿汉式

这种方式比较常用, 但是容易产生垃圾对象, 饿汉式是线程安全的, 不是懒加载的

优点: 没有加锁, 执行效率会高

缺点: 类加载的时候就初始化, 浪费内存

懒汉式基于 classloader 机制避免了多线程的同步问题, 不过, instance 在类加载的时候就实例化了

java 复制代码
public class Singleton3 {
    private static Singleton3 singleton3 = new Singleton3();

    private Singleton3() {
    }

    public static Singleton3 getSingleton3() {
        return singleton3;
    }
}

八、双检锁/双重校验锁

要求 jdk 必须大于 1.5, 这种方式是懒加载 且 线程安全的

这种方式采用双锁机制, 安全且在多线程的情况下能保持高性能

java 复制代码
public class Singleton4 {
    private volatile static Singleton4 singleton4;

    private Singleton4() {}

    public static Singleton4 getSingleton4() {
        if(singleton4 == null){
            synchronized (Singleton4.class) {
                if (singleton4 == null) {
                    singleton4 = new Singleton4();
                }
            }
        }
        return singleton4;
    }
}

九、登记式/静态内部类

这种方式不仅是懒加载的, 而且是线程安全的, 这种方式能达到双检锁方式一样的效果, 但是实现起来更简单。

这种方式只适用于静态域的情况, 双检锁的方式可以在实例域需要延迟初始化时使用。

这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟第 饿汉式式 不同的是:第 饿汉式式 只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。想象一下,如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第 3 种方式就显得很合理。

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

    private static class SingletonHolder {
        static final Singleton5 INSTANCE = new Singleton5();
    }

    public static final Singleton5 getInstance() {
        return SingletonHolder.INSTANCE;
    }

    public static void main(String[] args) {
        Singleton5Runnable singleton5Runnable = new Singleton5Runnable();
        new Thread(singleton5Runnable).start();
    }
}

输出:

text 复制代码
com.kino.designpattern.single.Singleton5@1873e6a0
com.kino.designpattern.single.Singleton5@1873e6a0
com.kino.designpattern.single.Singleton5@1873e6a0

十、枚举

要求 jdk1.5 及以上, 不是懒加载的, 是多线程安全的

这种方式没有被广泛采用, 但是这是实现单例模式的最佳方式, 它更简洁, 这种方式是 《Effective Java》作者Josh Bloch 提倡的方式, 它不仅仅能够避免多线程同步问题, 而且还自动支持序列机制, 防止反序列化重新创建的对象, 绝对防止多次实例化

java 复制代码
public enum Singleton6 {
    instance;
    private EnumResource enumResource;

    private Singleton6() {
        enumResource = new EnumResource();
    }

    public EnumResource getInstance() {
        return enumResource;
    }
}

class EnumResource {
    public static void main(String[] args) {
        System.out.println(Singleton6.instance.getInstance());
        System.out.println(Singleton6.instance.getInstance());
        System.out.println(Singleton6.instance.getInstance());
    }
}

输出:

text 复制代码
com.kino.designpattern.single.EnumResource@4554617c
com.kino.designpattern.single.EnumResource@4554617c
com.kino.designpattern.single.EnumResource@4554617c

十一、总结

一般情况下,不建议使用 懒汉方式, 建议使用 饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用 登记方式。如果涉及到反序列化创建对象时,可以尝试使用 枚举方式。如果有其他特殊的需求,可以考虑使用 双检锁方式。

相关推荐
blammmp9 分钟前
Java:数据结构-枚举
java·开发语言·数据结构
暗黑起源喵27 分钟前
设计模式-工厂设计模式
java·开发语言·设计模式
WaaTong32 分钟前
Java反射
java·开发语言·反射
九圣残炎1 小时前
【从零开始的LeetCode-算法】1456. 定长子串中元音的最大数目
java·算法·leetcode
wclass-zhengge1 小时前
Netty篇(入门编程)
java·linux·服务器
Re.不晚2 小时前
Java入门15——抽象类
java·开发语言·学习·算法·intellij-idea
雷神乐乐2 小时前
Maven学习——创建Maven的Java和Web工程,并运行在Tomcat上
java·maven
码农派大星。2 小时前
Spring Boot 配置文件
java·spring boot·后端
顾北川_野2 小时前
Android 手机设备的OEM-unlock解锁 和 adb push文件
android·java
江深竹静,一苇以航2 小时前
springboot3项目整合Mybatis-plus启动项目报错:Invalid bean definition with name ‘xxxMapper‘
java·spring boot