创建型模式丨单例模式

前言

单例模式(Singleton Pattern),属于创建型模式的一种,我觉得它是 23 种设计模式中最容易理解的模式。

定义与特点

定义:指一个类只有一个实例,且该类能自行创建这个实例的一种模式。
特点:

  • 单例类只能有一个实例对象;
  • 该单例对象必须由单例类自行创建;
  • 单例类对外只提供一个访问它的全局访问点;
  • 单例类隐藏所有的构造方法。

编码实现

通常,普通类的构造函数是公有的,外部类可以通过“new 构造函数()”来生成多个实例。但是,如果将类的构造函数设为私有的,外部类就无法调用该构造函数,也就无法生成多个实例。这时该类自身必须定义一个静态私有实例,并向外提供一个静态的公有函数用于创建或获取该静态私有实例。

常见的实现单例模式的方法有:饿汉式、懒汉式、双重锁定、静态内部类、枚举和使用 ThreadLocal。

饿汉式

public class Singleton {
    private static Singleton instance = new Singleton();

    private Singleton() {
    }

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

饿汉式,就是类一旦加载,不管需要不需要,就把单例初始化完成,保证使用 getInstance()的时候,单例是已经存在的了。

虽然是线程安全的,可以直接用于多线程而不会出现问题;但是类加载时就初始化,浪费内存,而且容易产生垃圾对象。

懒汉式

public class Singleton {
    private static Singleton instance = null;

    private Singleton() {
    }

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

懒汉式,先声明只一个初始值为 null 的 instance 遍历,只有当需要使用 getInstance()的时候,判断变量是否已经被初始化,没有初始化才 new 一个实例。

虽然按需加载,避免了内存浪费,减少了类初始化时间;但是它是非线程安全的,必须使用关键字 synchronized 同步才能保证单例,而加锁又会影响效率。

双重锁定

为了解决懒汉式加锁影响效率这个问题,双重检查锁定出现了。

public class Singleton {
    private static Singleton instance = null;

    private Singleton() {
    }

    public static Singleton getInstance() {
        // 第一次检查
        if (instance == null) {
            synchronized (Singleton.class) {
                // 第二次检查
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

此时为了减少锁的判断量,只需要对单例进行判断即可,如果不为空直接返回,如果为空,那就创建新的单例对象。
虽然对单重锁效率提升不少,但是比较耗时耗内存。

但是,这种写法依然不够稳妥。
JVM 底层为了优化程序运行效率,可能对上段代码进行指令重排序,在一些特殊情况下可能会出现 instance 遍历并没有完全完成初始化。为了防止这种情况,给 instance 变量加上 volatile 关键字。
volatile 关键字有两个作用:被它修饰的变量保证对所有线程可见;禁止指令重排序优化。

public class Singleton {
    // 禁止指令重排序优化
    private static volatile Singleton instance = null;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

静态内部类

除了双检锁方式外,还有一种比较常见的静态内部类方式保证懒汉式单例的线程安全:

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

    private Singleton() {
    }

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

这种写法可以达到双重锁定一样的效果,而且避免了 synchronized 带来的性能影响;这种实现方法既保证了线程安全,又能做到延迟加载。

枚举

public enum Singleton {
    INSTANCE;

    // 枚举同普通类一样,可以有自己的成员变量和方法
    public void doSomething() {
        System.out.println("推荐使用枚举实现单例");
    }
}

这是实现单例模式的最佳方法。代码更简洁,线程安全,支持序列化机制,可防止反序列化和反射的破坏。

使用 ThreadLocal

这种实现是线程单例,不是全局单例。也就是说,在一个线程只存在一个单例对象,不同的线程有不同的单例对象。

import java.util.HashMap;
import java.util.Map;

public class AppContext {
    private static final ThreadLocal<AppContext> local = new ThreadLocal<>();

    private Map<String, Object> data = new HashMap<>();

    public Map<String, Object> getData() {
        return getAppContext().data;
    }

    //批量存数据
    public void setData(Map<String, Object> data) {
        getAppContext().data.putAll(data);
    }

    //存数据
    public void set(String key, String value) {
        getAppContext().data.put(key, value);
    }

    //取数据
    public void get(String key) {
        getAppContext().data.get(key);
    }

    //初始化的实现方法
    private static AppContext init() {
        AppContext context = new AppContext();
        local.set(context);
        return context;
    }

    //做延迟初始化
    public static AppContext getAppContext() {
        AppContext context = local.get();
        if (null == context) {
            context = init();
        }
        return context;
    }

    //删除实例
    public static void remove() {
        local.remove();
    }
}

现在绝大多数单例模式的实现基本上都是采用的 ThreadLocal 这一种实现方式。

单例模式的替代方案

在不需要维持任何状态下,仅仅⽤于全局访问,那么使⽤静态类的⽅式更加⽅便。

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class Singleton {
    public static Map<String, String> cache = new ConcurrentHashMap<>();
}

优点与缺点

优点:

  • 可以保证内存里只有一个实例,减少了内存的开销。
  • 对有限资源的合理利用,保护有限的资源,防止资源重复竞抢。
  • 具备全局唯一访问点的权限控制,方便按照统一规则管控权限。

缺点:

  • 不支持有参数的构造函数。
  • 一般没有接口,扩展困难。
  • 耦合性高,因为隐蔽了不同对象之间的调用关系。

应用场景

  • 在计算机系统中,Windows 的回收站、资源管理器、系统中的缓存等常常被设计成单例。
  • 在 Spring 框架中,Bean 默认情况下就是单例模式。
  • 保存一些全局性的属性设置。
  • 某些类创建实例时占用资源较多,或实例化耗时较长,且经常使用。

注意:某类需要频繁实例化,而创建的对象又频繁被销毁,例如多线程的线程池、网络连接池、数据库连接池等,这种不是单例设计模式,而是【池化技术】。
参考:知乎

参考

C语言中文网
CSDN
趣学设计模式 — 黄靖锋
打赏
评论区
头像
文章目录