23中设计模式(1):单例模式

定义:确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

单例模式的设计要点

  • 构造方法私有化。
  • 有指向自己实例的静态私有引用。
  • 有对外提供自身实例的静态公有方法。

根据实例化对象时机的不同分为三种:

一种是饿汉式单例,一种是懒汉式单例,还有一种是枚举实现,它是饿汉式单例的一种特殊情况。

  • 饿汉式单例,在单例类被加载时候,就实例化一个对象交给自己的引用。

    /**
     * 饿汉式单例(可以使用)
     *
     * @author suvue
     * @date 2020/1/9
     */
    public class HungryStyle {
        private static HungryStyle instance = new HungryStyle();
    
        private HungryStyle() {
        }
    
        public static HungryStyle getInstance() {
            return instance;
        }
    }
    
  • 懒汉式单例,是指在调用取得实例方法的时候才会实例化对象。其实懒汉模式的单例创建有很多种,这里列举推荐使用的中l两种方式。

    双重检索方式

    结合了其余方式做了改进,其他方式的缺点如将同步锁加到getInstance()方法上,会导致速率很慢;而不进行双重检索(也就是不进行第二次校验),就会有线程安全问题,假如一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。

    /**
     * 懒汉式单例(双重检索,推荐使用)
     *
     * @author suvue
     * @date 2020/1/9
     */
    public class DoubleCheckStyle {
        //volatile的使用是为了防止指令重排。
        private static volatile DoubleCheckStyle instance = null;
    
        private DoubleCheckStyle() {
        }
    
        public static DoubleCheckStyle getInstance() {
            if (instance == null) {
                synchronized (DoubleCheckStyle.class) {
                    if (instance == null) {
                        //new对象的过程可拆解为三个过程:
                        //1.为新对象分配内存空间
                        //2.将变量引用的指针指向内存地址。
                        //3.实例化对象的一系列过程
                        //假如一个线程执行了1、2,还没来得及执行3,这时为它分配的执行时间
                        //用完了,另一个线程进来,此时因为上一个线程执行了2,那么它会以为已经
                        //实例化好对象了,就开心的拿着这个单例对象执行操作去了,实际上只分配了
                        //内存空间和引用,而没有进行实例化,所以这个线程用的时候就会抛异常。
                        instance = new DoubleCheckStyle();
                    }
                }
            }
            return instance;
        }
    
    }
    

    静态内部类方式

    这种方式和饿汉式单例一样,都是采用类加载机制,但是不同的是,饿汉式在类加载时进行初始化,静态内部类方式在调用getInstance方法时才会实例化。

    优点:兼顾了懒汉模式的内存优化(使用时才初始化)以及饿汉模式的安全性(类的静态属性只会在第一次加载类的时候初始化,所以JVM帮助我们保证了线程的安全性)。

    缺点:需要两个类去完成这一实现,虽然不会创建静态内部类的对象,但是其 Class 对象还是会被创建,而且是属于永久带的对象。因此创建好的单例,一旦在后期被销毁,不能重新创建。

    /**
     * 懒汉式单例(通过静态内部类的方式实现,推荐使用)
     *
     * @author suvue
     * @date 2020/1/9
     */
    public class StaticInnerStyle {
        private StaticInnerStyle() {
    
        }
    
        private static class InnerInstance {
            private final static StaticInnerStyle INSTANCE = new StaticInnerStyle();
        }
    
        public static StaticInnerStyle getInstance() {
            return InnerInstance.INSTANCE;
        }
    }
    
  • 枚举式单例借助JDK1.5中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。

    package cn.suvue.discipline.practice.designpattern.singleton;
    
    /**
     * 枚举方式实现的单例
     *
     * @author suvue
     * @date 2020/1/9
     */
    public class EnumStyle {
        private EnumStyle() {
        }
    
        private enum Singleton {
            /**
             * 单例
             */
            INSTANCE;
            private final EnumStyle instance;
    
            Singleton() {
                this.instance = new EnumStyle();
            }
    
            private EnumStyle getInstance() {
                return instance;
            }
        }
    
        public static EnumStyle getInstance() {
            return Singleton.INSTANCE.getInstance();
        }
    }
    
    

单例模式的优点

  • 在内存中只有一个对象,节省内存空间。
  • 避免频繁的创建销毁对象,可以提高性能。

使用注意事项

  • 只能使用单例类提供的方法得到单例对象,不要使用反射,否则将会实例化一个新对象。我们的代码在反射面前就是裸奔的,它是一种非常规操作。

  • 构造方法时私有的,因此单例类不可被继承。

  • 多线程使用单例使用共享资源时,注意线程安全问题。

  • 防止反射对单例造成破坏的方法,因为反射是基于构造方法拿到的实例,所以我们可以这么改一下:

    private StaticInnerStyle() {
            if (getInstance()!=null){
                throw new RuntimeException("调用失败");
            }
        }
    

单例模式在spring中应用

spring中使用的单例模式的懒加载,但是使用的是单例注册表实现的,先来看一个小例子。

package cn.suvue.discipline.practice.designpattern.singleton;


import java.util.concurrent.ConcurrentHashMap;
/**
 * 注册表实现的单例
 *
 * @author suvue
 * @date 2020/1/9
 */
public class RegisterStyle {
    private static ConcurrentHashMap<String, Object> register = new ConcurrentHashMap<String, Object>(32);

    static {
        RegisterStyle res = new RegisterStyle();
        register.put(res.getClass().getName(), res);
    }

    private RegisterStyle() {
    }

    public static RegisterStyle getInstance(String name) {
        if (name == null) {
            name = "cn.suvue.discipline.practice.designpattern.singleton.RegisterStyle";
        }
        if (register.get(name) == null) {
            try {
                register.put(name, Class.forName(name).newInstance());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return (RegisterStyle) register.get(name);
    }
}

上述的代码很简单,实现思路大同小异,唯一的不同是用到了ConcurrentHashMap。下面我们来看一下Spring中的应用。最经典的就是BeanFactory的获取bean的时候。

@SuppressWarnings("unchecked")
    protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
            @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {

        //校验bean名是否有非法字符
        final String beanName = transformedBeanName(name);
        Object bean;

        Object sharedInstance = getSingleton(beanName);
        if (sharedInstance != null && args == null) {
            //...
            //获取bean的实例
            bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
        }

        else {
            //...

            try {
                //获取并检查bean的定义
                final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
                checkMergedBeanDefinition(mbd, beanName, args);

                //...

                // 创建bean的实例 类定义是单例的情况.
                if (mbd.isSingleton()) {
                    sharedInstance = getSingleton(beanName, () -> {
                        try {
                            return createBean(beanName, mbd, args);
                        }
                        catch (BeansException ex) {
                            //出错了要销毁bean
                            destroySingleton(beanName);
                            throw ex;
                        }
                    });
                    //获取bean的实例
                    bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
                }

                //多例情况 这里不多分析
                else if (mbd.isPrototype()) {
                     //...
                }

                //作用域相关代码,这里不看它
                //...
            }
            catch (BeansException ex) {
                cleanupAfterBeanCreationFailure(beanName);
                throw ex;
            }
        }

        // Check if required type matches the type of the actual bean instance.
        if (requiredType != null && !requiredType.isInstance(bean)) {
            //校验bean的类型是否与实际的相匹配
            //...
        }
        return (T) bean;
    }

上面是我简化过的代码,我们着重看下是单例情况,也就是getSingleton方法的具体实现。

/** 单例对象的缓存容器,key是bean的名称,value是bean的实例 */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);


public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
        Assert.notNull(beanName, "Bean name must not be null");
        synchronized (this.singletonObjects) {
            Object singletonObject = this.singletonObjects.get(beanName);
            if (singletonObject == null) {
                //...
                boolean newSingleton = false;
                //...
                try {
                    //这里实际上创建一个新的对象
                    //因为这里的singletonFactory
                    //实际上传进来的是createBean(beanName, mbd, args)
                    singletonObject = singletonFactory.getObject();
                    newSingleton = true;
                }
                catch (IllegalStateException ex) {
                    //创建实例因为容器中已经存在了,就抛出异常,然后到容器中直接取单例对象
                    singletonObject = this.singletonObjects.get(beanName);
                    if (singletonObject == null) {
                        throw ex;
                    }
                }
                //...
                if (newSingleton) {
                    //如果是新的实例对象,那么就添加到容器中
                    addSingleton(beanName, singletonObject);
                }
            }
            return singletonObject;
        }
    }

下面我们看看spring是怎么往容器中放单例对象的。

protected void addSingleton(String beanName, Object singletonObject) {
        synchronized (this.singletonObjects) {
            //直接在同步代码块 执行put操作
            //上段代码的getSingleton方法 用了一个synchronized
            //本段代码中addSingleton方法 也用了一个synchronized
            //并且锁的对象都是singletonObjects
            this.singletonObjects.put(beanName, singletonObject);
            //下面的代码可以不用看,重要的是上面这行
            this.singletonFactories.remove(beanName);
            this.earlySingletonObjects.remove(beanName);
            this.registeredSingletons.add(beanName);
        }
    }

以上是我的一些粗鄙观点,希望看到本博文的大神能纠正其中的错误!万分感谢哦,以后我对单例有了新的认知了,会不断来补充更新的,也希望大神们多提提意见!

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

推荐阅读更多精彩内容