目录
一 什么是单例模式
保证一个类仅有一个实例,并提供一个访问它的全局访问点,一般来说是指进程内只允许创建一个对象。
二 使用单例模式的优点
- 减少了类的频繁创建,降低了系统资源开销。
- 类的实例变少,减少内存开销,降低了 GC 操作。
- 使用单例来表示全局唯一类和处理资源访问冲突。
三 单例模式的实现方式
单例实现指导原则:
- 构造函数 private 访问权限,避免外部通过 new 创建实例。
- 对象创建必须考虑是否线程安全。
- 对象创建是否支持懒加载。
- 获取对象函数是否高性能。
3.1 懒汉式-线程不安全版
核心是先不初始化单例,等第一次使用的时候再初始化,也就是“懒加载”模式。
public class Singleton {
private static Singleton singleton = null;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
基础版中,if 语句存在竞态条件,适用于单线程,多线程出现线程不安全的问题。
3.2 懒汉式-线程安全版
public class Singleton {
private static Singleton singleton = null;
private Singleton() {
}
public synchronized static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
加上 synchronized 之后能保证线程安全,但并发性能极差,事实上完全退化到了串行。
3.3 懒汉式-双重检查加锁版
public class Singleton {
private static volatile Singleton singleton = null;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
针对线程安全版的性能问题,我们采用 volatile 来保证可见性,防止指令重排序问题;双重检查来保证性能。
3.4 饿汉式
public class Singleton {
private static final Singleton singleton = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return singleton;
}
}
线程安全(得益于类加载机制),写起来超级简单,使用时没有延迟;坏处是有可能造成资源浪费(如果类加载后就一直不使用单例的话)。
值得注意的时,单线程环境下,饿汉与饱汉在性能上没什么差别;但多线程环境下,由于懒汉需要加锁,饿汉的性能反而更优。
饿汉模式能解决以下两种情况:
- 将耗时的初始化操作,提前到程序启动的时候完成,这样就能避免在程序运行的时候,再去初始化导致的性能问题。
- 更早的暴露因为对象实例占用资源多,而出现的 OOM 问题,提高系统可用性。
3.5 Holder模式
Holder 既能实现线程安全,又能通过懒加载规避资源浪费。
public class Singleton {
private Singleton() {
}
private static class SingletonHolder {
private static final Singleton singleton = new Singleton();
private SingletonHolder() {
}
}
public static Singleton getInstance() {
return SingletonHolder.singleton;
}
}
3.6 枚举
public enum Singleton {
uniqueInstance;
}
使用枚举来实现单例控制会更加简洁,并且无偿地提供了序列化的机制,并由 JVM 从根本上提供保障,绝对防止多次实例化,是更简洁、高效、安全的实现单例的方式
四 实现线程唯一的单例
- 一般来说单例类对象是进程唯一的,一个进程只能有一个单例对象 A,不同进程可以有相同的单例对象 A。
- 线程唯一的单例是说,在一个线程里面只能有一个单例对象 B,不同线程可以有相同的单例对象 B。
我们通过一个 HashMap 来存储对象,其中 key 是线程 ID,value 是对象。这样我们就可以做到,不同的线程对应不同的对象,同一个线程只能对应一个对象。实际上,Java 语言本身提供了 ThreadLocal 工具类,可以更加轻松地实现线程唯一单例。不过,ThreadLocal 底层实现原理也是基于下面代码中所示的 HashMap。
import java.util.concurrent.ConcurrentHashMap;
public class Singleton {
private static final ConcurrentHashMap<Long, Singleton> instances = new ConcurrentHashMap<>();
public Singleton() {
}
public static Singleton getInstance() {
Long currentThreadId = Thread.currentThread().getId();
instances.putIfAbsent(currentThreadId, new Singleton());
return instances.get(currentThreadId);
}
}
五 实现一个多例模式
“单例”指的是,一个类只能创建一个对象。对应地,“多例”指的就是,一个类可以创建多个对象。
import java.util.HashMap;
import java.util.Map;
public class Multi {
private long serverNo;
private String serverAddress;
private static final Map<Long, Multi> serverInstances = new HashMap<>();
static {
serverInstances.put(1L, new Multi(1L, "127.0.0.1:6379"));
serverInstances.put(2L, new Multi(2L, "127.0.0.1:6380"));
serverInstances.put(3L, new Multi(3L, "127.0.0.1:6381"));
}
private Multi(long serverNo, String serverAddress) {
this.serverNo = serverNo;
this.serverAddress = serverAddress;
}
public Multi getInstance(long serverNo) {
return serverInstances.get(serverNo);
}
}
对于多例模式,还有一种理解方式:同一类型的只能创建一个对象,不同类型的可以创建多个对象。这里的“类型”如何理解呢?
public class Logger {
private static final ConcurrentHashMap<String, Logger> instances = new ConcurrentHashMap<>();
private Logger() {}
public static Logger getInstance(String loggerName) {
instances.putIfAbsent(loggerName, new Logger());
return instances.get(loggerName);
}
public void log() {
//...
}
}
//l1==l2, l1!=l3
Logger l1 = Logger.getInstance("User.class");
Logger l2 = Logger.getInstance("User.class");
Logger l3 = Logger.getInstance("Order.class");
这种多例模式的理解方式有点类似工厂模式。它跟工厂模式的不同之处是,多例模式创建的对象都是同一个类的对象,而工厂模式创建的是不同子类的对象。