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. 单例模式与单一原则有冲突。单例模式把“要单例”和业务逻辑融合在一个类中了。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容