一、什么是单例模式
允许存在一个和仅存在一个给定类的实例,并提供了一个全局访问点供外部获取该实例。
一般分为两大类:
- 懒汉模式:实例在第一次使用时创建
- 饿汉模式:实例在类加载时创建
二、为什么要使用单例模式
1、资源有限。比如数据库的访问权限,如果不限制实例的数量,那么有限的资源很快就会耗尽,同时造成大量的对象处于等待状态。
2、全局唯一对象。比如系统要求提供一个唯一的序列号生成器。
三、UML图
比如生成全局唯一序列号的UML图如下所示
四、代码实现
要实现一个单例模式,谨记三点:
1、构造函数私有。
2、内部持有一个私有的静态单例模式的实例。
3、提供一个公共的静态方法用于获取单例对象。
实现进程间唯一的方式
1、饿汉式单例
public final class Singleton {
/**
* 使用private将构造方法私有化,以防外界通过该构造方法创建多个实例
*/
private Singleton() {}
/**
* 饿汉式:不支持延迟加载
* 优点:类加载都内存后,就实例化一个单例,JVM保证线程安全,简单实用,推荐使用
* 缺点:
* 由于instance的初始化是在类加载时进行的,类加载是由ClassLoader来实现的,如果初始化太早,就会造成资源浪费
* 类加载的时机:
* 1、new一个对象时
* 2、使用反射创建它的实例时
* 3、子类被加载时,如果父类还没有被加载,就先加载父类
* 4、jvm启动时执行主类会先被加载
*
*/
// 私有化的类成员变量
private static final Singleton instance = new Singleton();
// 公共的类实例的访问方法
public static Singleton getInstance() {
return instance;
}
private AtomicLong atomicLong = new AtomicLong(0);
public long getId() {
return atomicLong.incrementAndGet();
}
}
2、懒汉式单例
public final class Singleton {
/**
* 使用private将构造方法私有化,以防外界通过该构造方法创建多个实例
*/
private Singleton() {}
/**
* 懒汉式:支持延时加载
* 有性能瓶颈
*/
private static volatile Singleton instance = null;
// 方法级别的锁,避免多线程导致多个实例的情况,影响效率
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
private AtomicLong atomicLong = new AtomicLong(0);
public long getId() {
return atomicLong.incrementAndGet();
}
}
3、双重校验锁单例
public final class Singleton {
/**
* 使用private将构造方法私有化,以防外界通过该构造方法创建多个实例
*/
private Singleton() {}
/**
* 懒汉式
* volatile的作用是禁止指令重排
*/
private static volatile Singleton instance = null;
/**
* 双重校验锁
* 既支持延迟加载、又支持高并发的单例
*/
public static Singleton getInstance() {
// 解决上面代码中的效率问题
if (instance == null) {
// 类级别的锁
synchronized (Singleton.class) {
// 防止可能出现多个实例的情况
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
private AtomicLong atomicLong = new AtomicLong(0);
public long getId() {
return atomicLong.incrementAndGet();
}
}
4、CAS实现单例
public final class Singleton {
/**
* 使用private将构造方法私有化,以防外界通过该构造方法创建多个实例
*/
private Singleton() {}
/**
* 采用CAS实现单例
*/
private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<>();
public static Singleton getInstance() {
for(;;) {
Singleton singleton = INSTANCE.get();
if (singleton != null) {
return singleton;
}
singleton = new Singleton();
if(!INSTANCE.compareAndSet(null, singleton)) {
return singleton;
}
}
}
private AtomicLong atomicLong = new AtomicLong(0);
public long getId() {
return atomicLong.incrementAndGet();
}
}
5、静态内部类实现单例
public final class Singleton {
/**
* 使用private将构造方法私有化,以防外界通过该构造方法创建多个实例
*/
private Singleton() {}
/**
* 静态内部类
* JVM保证单例
* 对于内部类SingletonHolder,它是一个饿汉式的单例实现,
* 利用了ClassLoader来保证同步,同时又能让开发者控制类加载的时机
*/
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
/**
* 从外部来看,是懒汉式式的实现
*/
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
private AtomicLong atomicLong = new AtomicLong(0);
public long getId() {
return atomicLong.incrementAndGet();
}
}
6、枚举实现单例(工作中建议使用)
public enum Singleton {
/**
* 枚举实现,最优方法,不仅可以解决线程同步,还可以防止反序列化
* 使用Singleton.INSTANCE.getId();
*/
INSTANCE;
private AtomicLong atomicLong = new AtomicLong(0);
public long getId() {
return atomicLong.incrementAndGet();
}
}
实现线间唯一的方式
7、ThreadLocal实现单例
public class ThreadSingleton {
private ThreadSingleton() {}
// 适合有大量单例对象需要统一管理的情况
private static final ThreadLocal<ThreadSingleton> THREAD_SINGLETON_THREAD_LOCAL= new ThreadLocal<>();
// 延迟初始化
public static ThreadSingleton getThreadSingleton() {
ThreadSingleton threadSingleton = THREAD_SINGLETON_THREAD_LOCAL.get();
if(threadSingleton == null) {
threadSingleton = init();
}
return threadSingleton;
}
// 初始化实现
private static ThreadSingleton init() {
ThreadSingleton threadSingleton = new ThreadSingleton();
THREAD_SINGLETON_THREAD_LOCAL.set(threadSingleton);
return threadSingleton;
}
// 删除实例
public static void remove() {
THREAD_SINGLETON_THREAD_LOCAL.remove();
}
private AtomicLong atomicLong = new AtomicLong(0);
public long getId() {
return atomicLong.incrementAndGet();
}
}
8、容器单例
public class ThreadSingleton {
private ThreadSingleton() {}
// 适合有大量单例对象需要统一管理的情况
private static final Map<Long, ThreadSingleton> INSTANCE = new ConcurrentHashMap<>();
public static ThreadSingleton getInstance() {
Long currentThreadId = Thread.currentThread().getId();
INSTANCE.putIfAbsent(currentThreadId, new ThreadSingleton());
return INSTANCE.get(currentThreadId);
}
private AtomicLong atomicLong = new AtomicLong(0);
public long getId() {
return atomicLong.incrementAndGet();
}
}
五、优点
1、单例模式可以保证内存里只有一个实例,减少了内存的开销。
2、对有限资源的合理利用,保护有限的资源,避免对资源的多重占用。
3、单例模式设置全局访问点,可以优化和共享的访问。
六、缺点
1、单例模式一般没有接口,扩展性差。
2、在并发测试中,不利于代码调试。
3、作为全局变量使用时,引用的对象越多,代码修改影响范围越大。
4、单例模式的代码一般写在一个类中,如果功能设计不合理,容易违背单一职责原则。
七、应用场景
1、需要频繁创建的一些类,使用单例可以降低系统的内存压力,减少 GC。
2、某类只要求生成一个对象的时候,如每一个公文件的主键、生成唯一序列号ID、任务的任务ID等。
3、某些类创建实例时占用资源较多,或实例化耗时较长,且经常使用。
4、某类需要频繁实例化,而创建的对象又频繁被销毁的时候,如多线程的线程池、数据库连接池、对象池等。
5、频繁访问数据库或文件的对象。如数据库连接池、应用程序的日志对象、系统中的缓存。
6、当对象需要被共享的场合。由于单例模式只允许创建一个对象,共享该对象可以节省内存,并加快对象访问速度。如 Web 中的配置对象、数据库的连接池等。
八、单例源码解析
之前介绍了多种实现单例模式的方法,现在看下JDK和Spring是怎么运用单例模式的。
1、JDK
JDK中java.lang.Runtime类就是一个饿汉式单例模式,它不准外部创建实例
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
private Runtime() {}
}
2、Spring
本文主要讲解单例模式,所以直入主题,关于Spring内容,参考后续文章或其他文章。
在spring-beans-5.1.19.RELEASE中,找到SingletonBeanRegistry接口,源码如下:
package org.springframework.beans.factory.config;
import org.springframework.lang.Nullable;
// 注册单例bean接口
public interface SingletonBeanRegistry {
void registerSingleton(String var1, Object var2);
// 我们需要关注的方法,找到它的实现类
@Nullable
Object getSingleton(String var1);
boolean containsSingleton(String var1);
String[] getSingletonNames();
int getSingletonCount();
Object getSingletonMutex();
}
找到getSingleton()的实现类代码如下:
// 关于Spring实例化bean可以在写Spring循环依赖和三级缓存详说
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
private static final int SUPPRESSED_EXCEPTIONS_LIMIT = 100;
// 单例对象容器 一级缓存
private final Map<String, Object> singletonObjects = new ConcurrentHashMap(256);
// 单例工厂容器 三级缓存
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap(16);
// 循环依赖单例对象容器 二级缓存
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap(16);
// 已注册的单例列表
private final Set<String> registeredSingletons = new LinkedHashSet(256);
// 正在创建的单例列表
private final Set<String> singletonsCurrentlyInCreation = Collections.newSetFromMap(new ConcurrentHashMap(16));
// 。。。。。。省略部分代码
@Nullable
public Object getSingleton(String beanName) {
return this.getSingleton(beanName, true);
}
// 双重校验锁实现单例
@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 一级缓存,根据bean从单例对象缓存池获取当前对象
Object singletonObject = this.singletonObjects.get(beanName);
// 如果当前bean正在创建过程中,而且缓存池中没有则继续
if (singletonObject == null && this.isSingletonCurrentlyInCreation(beanName)) {
// 二级缓存
singletonObject = this.earlySingletonObjects.get(beanName);
// 从缓存池中获取bean实例,如果为null,对单例对象池加锁,然后再从缓存池中获取bean
if (singletonObject == null && allowEarlyReference) {
// 对单例对象池加锁
synchronized(this.singletonObjects) {
singletonObject = this.singletonObjects.get(beanName);
// 如果还是为null,就创建一个bean
if (singletonObject == null) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null) {
// 三级缓存
ObjectFactory<?> singletonFactory = (ObjectFactory)this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
}
}
return singletonObject;
}
// 。。。。。。省略部分代码
}
注意:Spring并没有私有构造方法,而是通过singletonFactory.getObject()返回具体beanName对应的ObjectFactory来创建bean。
JDK单例模式和Spring单例bean的区别:
- JDK单例模式:在一个JVM进程中仅有一个实例;
- Spring单例bean:在一个Spring容器中仅有一个实例。
九、注意点
1、如何破坏单例模式?
- 序列化
- 反射
2、如何防止呢?
- 实现readResolve方法防止序列化机制
public Singleton readResolve() {
return instance;
}
- 在私有构造函数内部添加一个成员变量flag标志防止反射二次调用
private Singleton() {
throw new RuntimeException("禁止通过反射调用!");
}