java 单例模式 1/23

1. 单例模式介绍

    很多时候,我们需要在应用中,保存一个唯一实例,例如后台服务进程需要一个全局的计数器,记录用户访问页面的点击量。如果计数器数量非常多,那么必然会在技术过程中出现冲突的情况。为了避免冲突,一个最简单的方法办法就是只有一个计数器实例,所有计数工作都有它来完成。

1.1 实现单例的方式
  1. 外部方式

外部程序使用某些全局变量时,做一些Try-Use的工作,如果没有,就自己创建一个,把它放在全局的位置上,如果有,就拿来直接用。

  1. 内部方式

类型自己控制生成实例的数量,无论客户程序是否尝试过了,类型自己控制只提供一个实例,客户程序使用的都是这个现成的唯一实例。

随着多核、集群的普遍应用,想通过简单的内部控制实现真正的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. 单例模式的优点和缺点

优点:
  1. 由于单例模式在内存中只有一个实例,因此降低了内存开销,特别是一个对象频繁的创建、销毁,而且创建或销毁时又无法性能优化。
  2. 当一个对象的产生需要比较多的资源时,比如读取配置、产生其他依赖对象时,则可以选择在应用启动时直接产生一个实例,然后永久驻留内存解决。
  3. 单例模式可以避免对资源的多重占用。
  4. 可以设置全局的访问点,优化共享资源的访问。
缺点:
  1. 单例模式没有接口,扩展很难。
  2. 单例模式对测试是不利的。
  3. 单例模式与单一原则有冲突。单例模式把“要单例”和业务逻辑融合在一个类中了。
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容