前言
单例模式(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
趣学设计模式 — 黄靖锋