Fork me on GitHub

【趣味设计模式系列】之【单例模式】

1. 简介

单例模式(Singleton):确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

2. 图解

类图如下:

3. 案例实现

单例特点:

  • 外部类不能随便对单例类创建,故单例的构造方法必须为private,在类的内部自行实例化;
  • 提供一个public方法入口,作为唯一调用单例类的途径得到实例。

3.1 饿汉式

package com.wzj.singleton;

/**
 * @Author: wzj
 * @Date: 2020/2/13 12:41
 * @Desc: 饿汉式
 *  类加载到内存后,就实例化一个单例,JVM保证线程安全
 *  唯一缺点:不管用到与否,类装载时就完成实例化
 */
public class HungrySingleton {

    private static final HungrySingleton HUNGRY_SINGLETON = new HungrySingleton();

    /**
     * 私有构造方法,只有本类才能调用
     */
    private HungrySingleton() {

    }

    public static HungrySingleton getInstance() {
        return HUNGRY_SINGLETON;
    }

    public static void main(String[] args) {
        HungrySingleton h1 = HungrySingleton.getInstance();
        HungrySingleton h2 = HungrySingleton.getInstance();
        System.out.println(h1 == h2);
    }
}

执行结果:

true
  • 分析:类加载到内存,就实例化一个单例,通过final的静态变量保证唯一实例。
  • 优点:线程安全。
  • 缺点:不管是否用到,类装载时就完成实例化。

3.2 懒汉式

package com.wzj.singleton;

/**
 * @Author: wzj
 * @Date: 2020/2/13 12:50
 * @Desc: 懒汉式-线程不安全
 */
public class LazySingleton {

    private static LazySingleton LAZY_SINGLETON;

    private LazySingleton() {
    }

    public static LazySingleton getInstance() {
        if (null == LAZY_SINGLETON) {
            //睡眠1毫秒,在new对象前,增加被其他线程打断的机会,保证能被多个线程执行
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            LAZY_SINGLETON = new LazySingleton();
        }

        return LAZY_SINGLETON;
    }

    public static void main(String[] args) {

        for (int i = 0; i < 100; i++) {
            new Thread(() ->
                    System.out.println(LAZY_SINGLETON.getInstance().hashCode())
            ).start();
        }
    }
}

执行结果

1338303845
1276043710
874014640
2116194883
835777993
527405373
38860249
772510047
484927678
1375039115
.
.
.
  • 分析:懒加载在调用getInstace方法的时候创建实例,通过100个线程测试发现其hashcode并不相等,并不是单例。
  • 优点:改善了饿汉式中实例不用也加载的弊端。
  • 缺点:引入了新的问题,线程不安全,并不能保证单例

3.3 synchronized修饰方法单例

package com.wzj.singleton;

/**
 * @Author: wzj
 * @Date: 2020/2/13 13:50
 * @Desc: 懒汉式-线程安全
 * 通过synchronized解决,但获取锁带来性能开销,效率下降
 */
public class ThreadSafeSingleton {

    private static ThreadSafeSingleton LAZY_SINGLETON;

    private ThreadSafeSingleton() {
    }

    public static synchronized ThreadSafeSingleton getInstance() {
        if (null == LAZY_SINGLETON) {
            //睡眠1毫秒,在new对象前,增加被其他线程打断的机会,保证能被多个线程执行
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            LAZY_SINGLETON = new ThreadSafeSingleton();
        }

        return LAZY_SINGLETON;
    }

    public static void main(String[] args) {

        for (int i = 0; i < 100; i++) {
            new Thread(() ->
                    System.out.println(LAZY_SINGLETON.getInstance().hashCode())
            ).start();
        }
    }
}

执行结果

2003664945
2003664945
2003664945
2003664945
2003664945
2003664945
2003664945
2003664945
.
.
.
  • 分析:通过synchronized关键字保证线程安全。
  • 优点:线程安全,保证单例。
  • 缺点:方法上加锁导致性能开销。

3.4 synchronized修饰代码块的单例

package com.wzj.singleton;

/**
 * @Author: wzj
 * @Date: 2020/2/13 13:50
 * @Desc: 懒汉式-线程不安全
 *  试图通过减小同步代码块的方式提高效率,带来了线程不安全
 */
public class ThreadUnSafeSingleton {

    private static ThreadUnSafeSingleton LAZY_SINGLETON;

    private ThreadUnSafeSingleton() {
    }

    public static  ThreadUnSafeSingleton getInstance() {
        if (null == LAZY_SINGLETON) {
            //试图通过减小同步代码块的方式提高效率,带来了线程不安全
            synchronized (ThreadUnSafeSingleton.class) {
                //睡眠1毫秒,在new对象前,增加被其他线程打断的机会,保证能被多个线程执行
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                LAZY_SINGLETON = new ThreadUnSafeSingleton();
            }

        }

        return LAZY_SINGLETON;
    }

    public static void main(String[] args) {

        for (int i = 0; i < 100; i++) {
            new Thread(() ->
                    System.out.println(LAZY_SINGLETON.getInstance().hashCode())
            ).start();
        }
    }
}

结果:

1276043710
1276043710
1276043710
1276043710
1276043710
1276043710
2116194883
2116194883
2116194883
2116194883
.
.
.
  • 分析:试图通过减小同步代码块的方式提高效率,带来了线程不安全。
  • 优点:减小了加锁的范围,提高了性能。
  • 缺点:线程不安全,结果显示不能保证单例。

3.5 双重检测安全单例

package com.wzj.singleton;

/**
 * @Author: wzj
 * @Date: 2020/2/13 13:50
 * @Desc: 双重检测下线程安全
 *
 */
public class DoubleCheckedThreadSafeSingleton {
    
    private volatile static DoubleCheckedThreadSafeSingleton LAZY_SINGLETON;

    private DoubleCheckedThreadSafeSingleton() {
    }

    public static DoubleCheckedThreadSafeSingleton getInstance() {
        if (null == LAZY_SINGLETON) {
            //试图通过减小同步代码块的方式提高效率,带来了线程不安全
            synchronized (DoubleCheckedThreadSafeSingleton.class) {
                if (null == LAZY_SINGLETON) {
                    //睡眠1毫秒,在new对象前,增加被其他线程打断的机会,保证能被多个线程执行
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    LAZY_SINGLETON = new DoubleCheckedThreadSafeSingleton();
                }
            }

        }

        return LAZY_SINGLETON;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() ->
                    System.out.println(LAZY_SINGLETON.getInstance().hashCode())
            ).start();
        }
    }
}

执行结果

527405373
527405373
527405373
527405373
527405373
527405373
527405373
527405373
.
.
.
  • 分析:第一个判空语句if (null == LAZY_SINGLETON),用来检测如果内存中有单例生成以后,永不进入下面的代码,直接走return语句返回已有的单例,第二个判空语句,保证当前线程拿到锁的前后,内存中都没有单例,才执行创建单例操作,防止中途被其他线程创建单例,进而重复创建;volatile关键字保证在执行语句LAZY_SINGLETON = new DoubleCheckedThreadSafeSingleton() 时,可以分解为如下的3行伪代码。
memory = allocate();  // 1:分配对象的内存空间
ctorInstance(memory); // 2:初始化对象
instance = memory;  // 3:设置instance指向刚分配的内存地址

上面3行伪代码中的2和3之间,可能会被重排序(在一些JIT编译器上,这种重排序是真实发生的)。2和3之间重排序之后的执行时序如下。

memory = allocate();  // 1:分配对象的内存空间
instance = memory;  // 3:设置instance指向刚分配的内存地址
// 注意,此时对象还没有被初始化!
ctorInstance(memory); // 2:初始化对象

上面的代码在编译器运行时,可能会出现重排序 从1-2-3 排序为1-3-2,如果发生重排序,另一个并发执行的线程B就有可能在判断instance不为null。线程B接下来将访问instance所引用的对象,但此时这个对象可能还没有被A线程初始化!

  • 优点:保证单例与线程安全。
  • 缺点:增加了代码的复杂度。

3.6 内部静态类单例

package com.wzj.singleton;

/**
 * @Author: wzj
 * @Date: 2020/2/13 15:45
 * @Desc:  静态内部类方式
 *  JVM保证单例
 *  加载外部类时不会加载内部类,这样可以实现懒加载
 */
public class InnerStaticClassSingleton {

    private InnerStaticClassSingleton() {

    }

    //内部静态类
    private static class InnerStaticClassSingletonHolder {
        private static final InnerStaticClassSingleton INSTANCE = new InnerStaticClassSingleton();
    }

    public static InnerStaticClassSingleton getInstance() {
        return InnerStaticClassSingletonHolder.INSTANCE;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() ->
                    System.out.println(InnerStaticClassSingleton.getInstance().hashCode())
            ).start();
        }
    }
}

执行结果:

772510047
772510047
772510047
772510047
772510047
772510047
772510047
772510047
772510047
772510047
.
.
.
  • 分析:因为虚拟机加载类的时候只加载一次,并且加载外部类时不会加载内部类,只有调用getInstance方法的时候,内部类才被加载,所以内部类的静态变量也只加载一次,JVM保证了单例与线程安全。
  • 优点:线程安全。
  • 缺点:无。

3.7 枚举单例

package com.wzj.singleton;

/**
 * @Author: wzj
 * @Date: 2020/2/13 16:05
 * @Desc: 枚举实现,既可以保证线程安全,还可以防止反序列化。
 */
public enum EnumSingleton {
    INSTANCE;
    public static void main(String[] args) {
        for(int i=0; i<100; i++) {
            new Thread(()->{
                System.out.println(EnumSingleton.INSTANCE.hashCode());
            }).start();
        }
    }
}
  • 优点:线程安全,同时保证不被反序列化,因为枚举类型没有构造方法,不能反序列化后创建对象。
  • 缺点:写法优点怪异。

4. 框架源码分析

以下源码分析基于Spring5.0.6 RELEASE。DefaultSingletonBeanRegistry.class部分源码如下.

    @Nullable
    public Object getSingleton(String beanName) {
        return this.getSingleton(beanName, true);
    }

    @Nullable
    protected Object getSingleton(String beanName, boolean allowEarlyReference) {
		// 从单例缓存容器中加载 bean
        Object singletonObject = this.singletonObjects.get(beanName);
		// 单例缓存容器中的 bean 为空,且当前 bean 正在创建
        if (singletonObject == null && this.isSingletonCurrentlyInCreation(beanName)) {
			// 加锁
            synchronized(this.singletonObjects) {
				// 从 earlySingletonObjects 容器中获取
                singletonObject = this.earlySingletonObjects.get(beanName);
				// earlySingletonObjects容器中没有,且允许提前创建
                if (singletonObject == null && allowEarlyReference) {
					// 从 singletonFactories 中获取对应的 ObjectFactory
                    ObjectFactory<?> singletonFactory = (ObjectFactory)this.singletonFactories.get(beanName);
                    // ObjectFactory 不为空,则创建 bean
					if (singletonFactory != null) {
						// 获取 bean
                        singletonObject = singletonFactory.getObject();
						// 添加 bean 到 earlySingletonObjects 中
                        this.earlySingletonObjects.put(beanName, singletonObject);
						// 从 singletonFactories 中移除对应的 ObjectFactory
                        this.singletonFactories.remove(beanName);
                    }
                }
            }
        }

        return singletonObject;
    }

  • 第一步,从singletonObjects 中,获取 Bean 对象。
  • 第二步,若为空且当前bean正在创建中,则从earlySingletonObjects中获取Bean对象。
  • 第三步,若为空且允许提前创建,则从singletonFactories中获取相应的ObjectFactory对象。若不为空,则调用其ObjectFactorygetObject(String name)方法,创建Bean对象,然后将其加入到earlySingletonObjects ,然后从singletonFactories删除。
    由此可见,Spring在创建单例bean的时候,采用的是双重检测加锁机制创建bean的。

5. 单例与静态方法的比较

  • 单例支持延迟加载,静态类第一次加载就初始化;
  • 单例常驻内存,除非JVM退出,静态方法中的对象,会随着静态方法执行完被释放,gc回收。

6. 应用场景

  • 数据库连接池,因为频繁建立或者关闭数据库连接,损耗性能非常大,因为何用单例模式来维护,就可以大大降低这种损耗;
  • 线程池,因为线程是一种稀缺资源,频繁创建线程,会导系统开销增大,线程之间的频繁切换也导致性能下降,由统一的线程池管理线程;
  • 开发中常用的配置工具类,因为配置类是共享的资源;
  • 日志应用,因为日志属于工享文件,一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加;
  • Windows的任务管理器,多次打开只会弹出一个对话框,确保系统由一个任务管理器管理;
  • 网站的计数器,如果多个,计数难以同步;
    综上,单例应用在共享资源上,要么方便管理,要么节约性能,避免不必要的性能开销。

7. 总结

单例的具体写法,需要结合场景与业务要求,确认是否支持线程安全,是否支持延迟加载,单例比较简单的写法是饿汉式,唯一的不足是不支持懒加载,还有静态内部类;比较完美的写法是双重检测加锁,虽然写法复杂,但支持延迟加载,线程安全,也是Spring源码使用的方式。

posted @ 2020-02-14 11:09  小猪爸爸  阅读(460)  评论(0编辑  收藏  举报