设计模式之单例模式详解

单例模式(Singleton Pattern)——设计模式中最简单,估计也是最常见的设计模式之一,属于创建型模式。一般来说,在程序的整个生命周期中,我们希望某些类有且只有一个的时候,这些类的设计就可以采用单例模式。这种类存在的原因可能是该类的创建需要消耗系统过多的资源、花费很多的时间,或者业务上客观就要求了只能有一个实例等,比如经常遇到的一个场景:应用中的配置文件读取,只在系统启动的时候读取这些配置文件,并将这些配置保存在内存中,以后在程序中使用这些配置文件信息的时候不再重新读取。

概述

单例模式——顾名思义单个实例,从名字中就能引出思考问题:怎么才能保证单个,首先肯定是该类在外部无法被实例化,将构造函数私有化(private)可以实现;但又需要唯一的一个实例,那就只能类自行能够创建,并且能够保证实例的唯一性;外部虽然不能实例化,但肯定要能访问,所以要提供该实例的全局访问方法。
所以一般来说包含下面三个要素:

  1. 私有的静态的实例对象 private static instance。
  2. 私有的构造函数(保证在该类外部,无法通过new的方式来创建对象实例) private Singleton(){}。
  3. 公有的、静态的、访问该实例对象的方法 public static Singleton getInstance(){}。

饿汉式

这也是最常见的一种实现方式:

public class Singleton {
	// 声明即创建
    private static Singleton instance = new Singleton();
	// 私有的构造函数
    private Singleton() {
    }
	// 公有的、静态的、访问该实例对象的方法
    public static Singleton getInstance() {
        return instance;
    }
	// 具体方法
    public void sayHello(){
        System.out.println("您好,我能为你做些什么?");
    }
}

下面来测试一下:

public class Test {
    public static void main(String[] args) {
        // 获取实例
        Singleton singleton = Singleton.getInstance();
        // 调用方法
        singleton.sayHello();
    }
}

运行结果:

您好,我能为你做些什么?

这种方式不管类有没有被使用,先创建实例,被形象的称为饿汉式。很明显此种方式能够保证线程安全,但不能延迟初始化,当类的创建消耗资源比较少,速度比较快的时候,缺点还不明显;如果类的创建耗时且占资源,并且类不止一个的时候,不但应用启动速度会受影响,对于没用使用就占用资源也是一种不合理。

懒汉式

正因为如此,我们稍作调整便产生了能够延迟初始化的懒汉式:应用刚启动的时候,并不创建实例,当外部调用该类的实例或者方法的时候,才创建该类的实例,以时间换空间。

public class Singleton {
	// 只声明不创建
    private static Singleton instance = null;
	// 私有的构造函数
    private Singleton() {
    }
	// 先判断实例是否存在
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

这种方式的使用跟饿汉式一样,这里要问提出一个问题:当被实际使用的时候才创建实例,那如果彼时有多个线程同时调用Singleton.getInstance()方法会产生什么情况?很有可能会产生多个实例。那如果解决这个问题呢?最先想到的简单方法:加锁。

public class Singleton {
    private static Singleton instance = null;

    private Singleton() {
    }

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

虽然synchronized的性能JDK高版本进行了优化,但总是会牺牲一点效率,并且是为了应对应用中的小概率事件,大部分时间instance == null是不成立的。所以这里我们对整个getInstance()加锁进行方法同步是不明智的,锁的粒度太大,很多线程同时访问的时候阻塞很严重,尤其在getInstance() 的性能对应用程序很关键的时候不建议频繁使用;我们只要保证实例的创建是唯一的,可以对必要代码块进行加锁。

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;
    }
}

这里使用了实例两次非空判断,一次在进入synchronized代码块之前,一次在进入synchronized代码块之后两次判断是为了避免,线程A在创建实例的过程中,线程B因为instance == null进入同步代码块等待创建实例,等A创建返回以后,B接着继续创建实例。那为什么不像下面这样设置同步代码块呢?

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

这样也是可以保证多线程下实例唯一,但是我们知道synchronized进行同步,意味着独占锁,同一时间只能一个线程执行同步块里面的代码,还有锁的争夺、释放等问题,是很消耗资源的。如果不在进入同步块之前进行非空判断,如果之前已经有线程创建了该类的实例,那每次的访问该实例的方法都要进入同步块,这样会非常的耗费性能。所以进入同步块之前加上了非空判断,发现实例已经存在了,就不必进入同步块了,直接实例即可,这种双重校验锁可以在线程安全且情况下保持高性能。

new Singleton()做了什么:

  1. 看class对象是否加载,如果没有就先加载class对象。
  2. 给Singleton实例分配内存,将函数压栈,并且申明变量类型。
  3. 初始化构造函数以及里面的字段,在堆内存开辟空间。
  4. 将instance对象指向分配的内存空间。

由于java编译器允许执行无序,并且jdk1.5之前的jvm(java内存模型)中的Cache,寄存器到主内存的回写顺序规定,3和4是无法保证按顺序执行的; 导致实例内存还没分配,就被使用了。
比如:线程A执行到new Singleton(),开始初始化实例对象,由于存在指令重排序,这次new操作,先把引用赋值了,还没有执行构造函数。这时时间片结束了,切换到线程B执行,线程B调用new Singleton()方法,发现引用不等于null,就直接返回引用地址了,然后线程B执行了一些操作,就可能导致线程B使用了还没有被初始化的变量。
这个问题已经修复了,SUN官方调整了JVM,加了volatile之后,就保证new 不会被指令重排序。因此在jdk1.5之前只需要写成这样既可, private volatitle static Singleton instance; 这样就可以保证每次都是从主内存中取,当然这样写或多或少的回影响性能,但是为了安全起见,这点性能牺牲还是值得

静态内部类式

上面的最优实现使用了synchronized同步块,并且用了双重非空校验,保证了懒汉式单例模式在多线程环境下的安全性和高性能,但还是想能不能再简单些,再优雅些。

public class Singleton {

    private Singleton() {
    }

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

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

这种方式能达到双检锁方式一样的功效,但实现更简单,利用了 classloader 机制来保证初始化 instance 时只有一个线程,不仅确保了线程的安全性,也能够保证对象的唯一性;同时只要应用中不使用内部类,JVM就不会去加载这个单例类,也就不会创建单例对象,从而实现延迟加载。对静态域使用延迟初始化,应使用这种方式,双检锁方式可在实例域需要延迟初始化时使用。

枚举式

介绍枚举方式之前,说一个单例模式被某些方法破解的问题,例如反射破解:

public class Test {

    public static void main(String[] args) {

		// 获取实例
		Singleton singleton1 = Singleton.getInstance();
		Singleton singleton2 = Singleton.getInstance();

		System.out.println(singleton1);
		System.out.println(singleton2);
		try {
			Class clazz = Class.forName("pattern.singleton.Singleton");
			Constructor c = clazz.getDeclaredConstructor();
			c.setAccessible(true);

			Singleton s1 = (Singleton)c.newInstance();
			Singleton s2 = (Singleton)c.newInstance();
			//通过反射,得到的两个不同对象
			System.out.println(s1);
			System.out.println(s2);
		}catch (Exception e){
			e.printStackTrace();
		}
    }
}

运行结果:

pattern.singleton.Singleton@2db0f6b2
pattern.singleton.Singleton@2db0f6b2
pattern.singleton.Singleton@3cd1f1c8
pattern.singleton.Singleton@3a4afd8d

从结果中可以看出通过反射可以生成多个实例,《effective java》中只简单的提了几句话:“享有特权的客户端可以借助AccessibleObject.setAccessible方法,通过反射机制调用私有构造器。如果需要低于这种攻击,可以修改构造器,让它在被要求创建第二个实例的时候抛出异常。”,也就是可以在构造函数中加入返回的实例对象非空判断,来防止别人通过反射创建多个实例对象。

private Singleton() {
    if (instance != null){
        throw new RuntimeException();
    }
}

同样通过反序列化机制也可以破解单例模式。简单来说“任何一个readObject方法,不管是显式的还是默认的,它都会返回一个新建的实例,这个新建的实例不同于该类初始化时创建的实例。”
《effective java》第77条:对于实例控制,枚举类型优于readResolve

由于 JDK1.5 之后才加入 enum 特性,这种实现方式还没有被广泛采用,不免让人感觉生疏,在实际工作中也很少用。但这是实现单例模式的最佳方法,它更简洁,自动支持序列化机制,绝对防止多次实例化。

public enum Singleton {
    INSTANCE;

    public void sayHello() {
        System.out.println("您好,我能为你做些什么?");
    }
}

测试一下:

public class Test {
    public static void main(String[] args) {
        Singleton singleton = Singleton.INSTANCE;
        singleton.sayHello();
    }
}

运行结果:

您好,我能为你做些什么?

这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。

至于枚举为什么可以防止反射和反序列化,这里先不详述了。简单的说也就是调用反射newInstance方法时会检查是否为枚举类,如果是将报错;在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。

登记式单例

Spring使用的是单例注册表方式(登记式单例),即维护一组单例类的实例,将这些实例存储到一个Map(登记簿)中,对于已经登记过的单例,则从工厂直接返回,对于没有登记的,则先登记,而后返回,这种方式并没有去改变类,所做的就是起到一个登记的作用。IOC容器就是做的这个事,你需要就找他去拿,他就可以很方便的实现Bean的管理。
看个例子:

public class RegSingleton {
    // 使用一个map来当注册表
    private static Map<String, Object> regMap;

    // 静态块,在类被加载时自动执行,把RegSingleton自己也纳入容器管理
    static {
        regMap = new ConcurrentHashMap<>(32);
        regMap.put(RegSingleton.class.getName(), new RegSingleton());
    }

    // 如果需要继承,则修改构造函数的可见性
    private RegSingleton() {
    }

    public static Object getInstance(String name) {
        if (name == null) {
            name = RegSingleton.class.getName();
        }
        if (regMap.get(name) == null) {
            try {
                regMap.put(name, Class.forName(name).newInstance());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return regMap.get(name);
    }
}

Spring源码,…为了节约篇幅。

public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport implements ConfigurableBeanFactory {
	...
    // name 要检索的bean的名称
	// requiredType 要检索的bean所需的类型
	// args 使用显式参数创建bean实例时使用的参数(仅在创建新实例时应用,而不是在检索现有实例时应用)
	// typeCheckOnly 是否为类型检查而获得实例,而不是实际使用
    protected <T> T doGetBean(String name, Class<T> requiredType, final Object[] args, boolean typeCheckOnly) throws BeansException {
    	// 返回bean名称,剥离工厂引用前缀,并将别名解析为规范名称
        final String beanName = this.transformedBeanName(name);
        // 从缓存中获取对象,调用了DefaultSingletonBeanRegistry的getSingleton方法
        Object sharedInstance = this.getSingleton(beanName);
        Object bean;
        if (sharedInstance != null && args == null) {
            if (this.logger.isDebugEnabled()) {
            	// 指定的singleton的bean是否正在创建
                if (this.isSingletonCurrentlyInCreation(beanName)) {
                    this.logger.debug("Returning eagerly cached instance of singleton bean '" + beanName + "' that is not fully initialized yet - a consequence of a circular reference");
                } else {
                    this.logger.debug("Returning cached instance of singleton bean '" + beanName + "'");
                }
            }
			// 完成FactoryBean的相关处理,并用来获取FactoryBean的处理结果
            bean = this.getObjectForBeanInstance(sharedInstance, name, beanName, (RootBeanDefinition)null);
        } else {
        	// 在当前线程中,返回指定的bean是否正在创建。
            if (this.isPrototypeCurrentlyInCreation(beanName)) {
                throw new BeanCurrentlyInCreationException(beanName);
            }
			// 检查该工厂是否存在bean定义
            BeanFactory parentBeanFactory = this.getParentBeanFactory();
            if (parentBeanFactory != null && !this.containsBeanDefinition(beanName)) {
                // 递归寻找
                String nameToLookup = this.originalBeanName(name);
                if (args != null) {
                    return parentBeanFactory.getBean(nameToLookup, args);
                }
				// 如果没有args,委托给标准的getBean方法
                return parentBeanFactory.getBean(nameToLookup, requiredType);
            }

            if (!typeCheckOnly) {
            	// 将指定的bean标记为已经创建(或即将创建)
                this.markBeanAsCreated(beanName);
            }

            try {
                final RootBeanDefinition mbd = this.getMergedLocalBeanDefinition(beanName);
                this.checkMergedBeanDefinition(mbd, beanName, args);
                // 先初始化当前bean所依赖的bean
                String[] dependsOn = mbd.getDependsOn();
                String[] var11;
                if (dependsOn != null) {
                    ...
                }
				// 单例的情况下
                if (mbd.isSingleton()) {
                	// 从缓存(singletonObjects)中去拿
                    sharedInstance = this.getSingleton(beanName, new ObjectFactory<Object>() {
                    	// 回调函数
                        public Object getObject() throws BeansException {
                            try {
                            	// 创建bean
                                return AbstractBeanFactory.this.createBean(beanName, mbd, args);
                            } catch (BeansException var2) {
                                AbstractBeanFactory.this.destroySingleton(beanName);
                                throw var2;
                            }
                        }
                    });
                    // 获取给定bean实例的对象,无论是bean实例本身,还是FactoryBean创建的对象
                    bean = this.getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
                } else if (mbd.isPrototype()) {
                    ... 
                } else {
    				... 
                }
            } catch (BeansException var23) {
                this.cleanupAfterBeanCreationFailure(beanName);
                throw var23;
            }
        }
		// 类型检查,没有问题就返回已经创建好的bean,此时这个bean是包含依赖关系的bean
        if (requiredType != null && bean != null && !requiredType.isAssignableFrom(bean.getClass())) {
            ... 
        } else {
            return bean;
        }
    }
}
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
	// 内部标记为一个空的单例对象:并发Map(不支持空值)作为标志值
    protected static final Object NULL_OBJECT = new Object();
    protected final Log logger = LogFactory.getLog(this.getClass());
    // 存放singleton对象的缓存 
    private final Map<String, Object> singletonObjects = new ConcurrentHashMap(256);
    // 存放制造singleton的工厂对象的缓存  
    private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap(16);
    // 存放singletonFactory制造出来的singleton的缓存
    private final Map<String, Object> earlySingletonObjects = new HashMap(16);
    // 单例注册表
    private final Set<String> registeredSingletons = new LinkedHashSet(256);
	...
	// 公有构造函数,可继承
    public DefaultSingletonBeanRegistry() {
    }
	// 注册单例对象,加锁,singletonObjects存在抛出异常,不存在则添加addSingleton
    public void registerSingleton(String beanName, Object singletonObject) throws IllegalStateException {
	...
    }
	// 添加单例对象,加锁
    protected void addSingleton(String beanName, Object singletonObject) {
	...
    }
    
	...

    protected Object getSingleton(String beanName, boolean allowEarlyReference) {
        // 先从singletonObjects获取
        Object singletonObject = this.singletonObjects.get(beanName);
        // 如果beanName对应的实例不存在但是正在创建
        if (singletonObject == null && this.isSingletonCurrentlyInCreation(beanName)) {
            synchronized(this.singletonObjects) {
            	// 再从earlySingletonObjects获取
                singletonObject = this.earlySingletonObjects.get(beanName);
                if (singletonObject == null && allowEarlyReference) {
                    ObjectFactory<?> singletonFactory = (ObjectFactory)this.singletonFactories.get(beanName);
                    if (singletonFactory != null) {
                    	// 再从singletonFactory获取
                        singletonObject = singletonFactory.getObject();
                        this.earlySingletonObjects.put(beanName, singletonObject);
                        this.singletonFactories.remove(beanName);
                    }
                }
            }
        }

        return singletonObject != NULL_OBJECT ? singletonObject : null;
    }
	...
}

singletonObjects指单例对象的cache,singletonFactories指单例对象工厂的cache,earlySingletonObjects指提前曝光的单例对象的cache,这三个cache构成了三级缓存,容量分别为256,16,16。
getSingleton()可以看到获取顺序:singletonObjects——earlySingletonObjects——singletonFactories 。
在singletonObjects中获取bean的时候,没有使用synchronized,而在singletonFactories和earlySingletonObjects中的操作都是在synchronized代码块中完成的,正好和他们各自的数据类型对应,singletonObjects使用的使用ConcurrentHashMap线程安全,而singletonFactories和earlySingletonObjects使用的是HashMap,线程不安全。

总结

单例模式实现比较简单,有点也很明显:

  1. 实例只有一个,节省一定的资源。
  2. 提供了对唯一实例的受控访问。
  3. 允许可变数目的实例。

但也存在缺点:

  1. 大部分实现模式不能被继承。
  2. 由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。
  3. 如果单例类的职责过重,在一定程度上违背了“单一职责”原则。
  4. 滥用单例将带来一些负面问题。

在使用的时候要注意:

  1. 不适用于变化的对象,不能保存对象的状态。
  2. 被反射等方法破解单例的可能。
  3. 线程安全问题。
  4. 单例对象生命周期较长,要考虑内存泄漏的问题。

通用使用经验:
一般情况下,类功能比较简单,资源消耗少的时候,可以直接使用饿汉式;明确需要延迟加载的时候,可以使用双检锁方式或静态内部类方式;如果涉及到反序列化创建对象时,可以枚举方式;至于Spring的单例注册表方式,平时应用中使用的可能性较小,但其方式实现的思想值得借鉴,当需要编写工具的时候可以参考。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值