1. 单例模式介绍
很多时候,我们需要在应用中,保存一个唯一实例,例如后台服务进程需要一个全局的计数器,记录用户访问页面的点击量。如果计数器数量非常多,那么必然会在技术过程中出现冲突的情况。为了避免冲突,一个最简单的方法办法就是只有一个计数器实例,所有计数工作都有它来完成。
1.1 实现单例的方式
- 外部方式
外部程序使用某些全局变量时,做一些
Try-Use
的工作,如果没有,就自己创建一个,把它放在全局的位置上,如果有,就拿来直接用。
- 内部方式
类型自己控制生成实例的数量,无论客户程序是否尝试过了,类型自己控制只提供一个实例,客户程序使用的都是这个现成的唯一实例。
随着多核、集群的普遍应用,想通过简单的内部控制实现真正的
Singleton
越来越难。
1.2 单例模式的范围
如果一个虚拟机里面有多个
ClassLoader
,而且都可以装载某个类的话,就算这个类是单例,它也会产生多个实例;如果一个机器上有多个虚拟机,那更不是单例了。
2. 单例模式的种类
2.1 懒汉式单例
懒汉式单例是指在类加载的时候不创建单例实例,只有在第一次请求实例的时候创建,并且在第一次创建后,以后不再创建该类的实例。
/**
* 描述:线程安全的Singleton类。该实例被懒加载,因此需要同步锁机制
*
* @author biguodong
* Create time 2018-10-22 下午4:04
**/
public final class LazySingleton {
private static LazySingleton instance = null;
/**
* 构造方法私有,防止外部构建
* 防止使用反射实例化
*/
private LazySingleton(){
if(instance == null){
instance = this;
}else{
throw new IllegalStateException("实例已存在!");
}
}
/**
* 只有在第一次调用实例的时才会创建,延迟加载
* @return
*/
public static synchronized LazySingleton getInstance(){
if(null == instance){
return new LazySingleton();
}
return instance;
}
}
2.2 饿汉式单例
类在加载时,实例已经被创建。
/**
* 描述:急切的创建可以保证线程安全,静态变量在类加载的时候已经被创建
*
* @author biguodong
* Create time 2018-10-22 下午4:17
**/
public final class EagerlySingleton {
private static final EagerlySingleton instance = new EagerlySingleton();
/**
* 构造方法私有
*/
private EagerlySingleton(){
}
/**
* 提供访问单例的静态方法
* @return
*/
public static EagerlySingleton getInstance(){
return instance;
}
}
2.3 登记式单例(spring单例的实现)
将单例维护在一个登记簿Map
中,已经登记过的单例,直接从工厂直接返回,没有登记的,先登记后返回。
/**
* 描述:spring-beans:Abstractfactory
*
* @author biguodong
* Create time 2018-10-22 下午4:25
**/
public class RegisterSingleton {
/**
* Internal marker for a null singleton object:
* used as marker value for concurrent Maps (which don't support null values).
*/
protected static final Object NULL_OBJECT = new Object();
/** Cache of singleton objects: bean name --> bean instance */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256);
/** Cache of early singleton objects: bean name --> bean instance */
private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16);
/** Cache of singleton factories: bean name --> ObjectFactory */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>(16);
/** Names of beans that are currently in creation */
private final Set<String> singletonsCurrentlyInCreation =
Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>(16));
/**
* Return the (raw) singleton object registered under the given name.
* <p>Checks already instantiated singletons and also allows for an early
* reference to a currently created singleton (resolving a circular reference).
* @param beanName the name of the bean to look for
* @param allowEarlyReference whether early references should be created or not
* @return the registered singleton object, or {@code null} if none found
*/
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
/**
* Return whether the specified singleton bean is currently in creation
* (within the entire factory).
* @param beanName the name of the bean
*/
public boolean isSingletonCurrentlyInCreation(String beanName) {
return this.singletonsCurrentlyInCreation.contains(beanName);
}
}
2.4 双重检查加锁(懒汉式)
因为懒汉式的实现是线程安全的,所以会降低整个访问速度,为了提高性能既保证安全,可以使用“双重检查加锁”实现。
是指不是每次进入
getInstance
方法都需要同步,而是先不同步。当进入方法后,先检测实例是否存在,如果不存在进入下面的同步块,这是第一重检查;进入同步块后再次检测实例是否存在,如果不存在,就在同步块内创建一个实例,这是第二重检查,这样一来,整个过程只需要一次同步,从而减少多次在同步情况下进行判断所浪费的时间。
volatile
修饰的变量不会被本地线程缓存
/**
* 描述:懒汉模式-双重检查枷锁
*
* @author biguodong
* Create time 2018-10-22 下午5:47
**/
public final class LazySingletonDoubleCheckLocking {
private static volatile LazySingletonDoubleCheckLocking instance;
/**
* 构造函数私有,防止外部调用
*/
private LazySingletonDoubleCheckLocking(){
if(null != instance){
throw new IllegalStateException("实例已存在!");
}
}
public static LazySingletonDoubleCheckLocking getInstance(){
/**
* 局部变量使性能提高25%
* Joshua Bloch "Effective Java, Second Edition", p. 283-284
*/
LazySingletonDoubleCheckLocking result = instance;
/**
* 检查实例是否已经创建存在,如果存在,直接return
*
*/
if(result == null){
/**
* 如果没有创建,我们不能保证同时是否有别的线程也在访问,因此,我们需要对一个对象加锁以实现互斥
*/
synchronized (LazySingletonDoubleCheckLocking.class){
result = instance;
if(result == null){
/**
* 此时对象仍然为空,我们可以安全(没有其他线程可以进入此区域)创建一个实例,并使它成为我们的单例
*/
result = instance = new LazySingletonDoubleCheckLocking();
}
}
}
return result;
}
}
2.5 一种更好的方式(懒汉式)
使用
lazy initialization holder class
既实现延迟加载,又保证线程安全
前面的饿汉式,因为在类加载的时候就去初始化对象,浪费了一定的内存空间;
采用静态初始化器的方式,它可以由
JVM
来保证线程的安全性。
/**
* 描述:线程安全的创建java单例
* 该技术尽可能的慵懒,适用于java的所有版本,利用了关于类初始化的特征,因此可以在所有符合java规范的编译器及虚拟机中工作
*
* 只有调用了getInstance方法,内部类才会被引用(因此类加载器不回加载),这个设计时线程安全的,不需要特殊的语言结构(synchronized, volatile)
* @author biguodong
* Create time 2018-10-22 下午6:14
**/
public final class LazySingletonInnerClass {
/**
* private constructor
*/
private LazySingletonInnerClass(){
}
public static LazySingletonInnerClass getInstance(){
return HelperHolder.INSTANCE;
}
/**
* 提供懒加载实例
*/
private static class HelperHolder{
private static final LazySingletonInnerClass INSTANCE = new LazySingletonInnerClass();
}
}
3. 单例和枚举
单元素的枚举类型已经成为实现单例模式的最佳方法。Java的枚举类型其实是功能齐全的类,因此可以有自己的属性和方法。java枚举类的基本思想是通过公有的静态final
域为每个枚举常量导出实例的类。从某个角度上讲,枚举是单例的泛型化,本质是单元素的枚举。
/**
* 描述:这个实现本身是线程安全的,但是添加其他方法及线程安全性是开发人员的责任
*
* @author biguodong
* Create time 2018-10-22 下午5:40
**/
public enum EnumSingleton implements SingletonInterface{
INSTANCE{
@Override
public String doSomething() {
return "Hello singleton";
}
};
public static EnumSingleton getInstance(){
return EnumSingleton.INSTANCE;
}
}
4. 单例模式的优点和缺点
优点:
- 由于单例模式在内存中只有一个实例,因此降低了内存开销,特别是一个对象频繁的创建、销毁,而且创建或销毁时又无法性能优化。
- 当一个对象的产生需要比较多的资源时,比如读取配置、产生其他依赖对象时,则可以选择在应用启动时直接产生一个实例,然后永久驻留内存解决。
- 单例模式可以避免对资源的多重占用。
- 可以设置全局的访问点,优化共享资源的访问。
缺点:
- 单例模式没有接口,扩展很难。
- 单例模式对测试是不利的。
- 单例模式与单一原则有冲突。单例模式把“要单例”和业务逻辑融合在一个类中了。