设计模式之(四):单例设计模式

一.什么是单例模式

1.什么是单例模式

单例模式就是一个类对外只有一个实例

2.场景
  • 实例化类需要消耗很多的资源
  • 该类只能实例化一次,对外是唯一的对象
3.表现形式:
  • 构造方法是私有的,即外部是无法实例化该类的
  • 有一个保存当前实例的私有静态变量
  • 有一个对外开放的访问该类对象的方法
4.单例的线程安全性检测

通过多线程调用单例的获取实例的方法,检查每次的实例是否相同

二.单例模式的实现方式

1.饿汉式(线程安全)

(1)特点:

  • 在类加载的时候就实例化类(感觉很饿一样,饭一到就直接吃)

(2)实例:

  1. /**
  2. * 饿汉式单例模式
  3. */
  4. public class HungrySingleton {
  5. /**
  6. * 在类加载时就实例化
  7. */
  8. private static final HungrySingleton INSTANCE = new HungrySingleton();
  9. /**
  10. * 构造方法外对私有
  11. */
  12. private HungrySingleton() {
  13. }
  14. /**
  15. * 对外开放的访问实例对象的方法
  16. */
  17. public static HungrySingleton getInstance() {
  18. return INSTANCE;
  19. }
  20. }

测试代码

  1. class HungrySingletonRunnable implements Runnable {
  2. @Override
  3. public void run() {
  4. System.out.println( HungrySingleton.getInstance());
  5. }
  6. }
  7. /**
  8. * 饿汉式测试
  9. * @author shixinke
  10. */
  11. public class HungrySingletonTest {
  12. public static void main(String[] args) {
  13. new Thread(new HungrySingletonRunnable()).start();
  14. new Thread(new HungrySingletonRunnable()).start();
  15. new Thread(new HungrySingletonRunnable()).start();
  16. }
  17. }

打印结果:

  1. com.shixinke.practise.design.pattern.creation.singleton.HungrySingleton@5eacd0a4
  2. com.shixinke.practise.design.pattern.creation.singleton.HungrySingleton@5eacd0a4
  3. com.shixinke.practise.design.pattern.creation.singleton.HungrySingleton@5eacd0a4

(3)优点

  • 是线程安全的

注:JVM保证一个类只会被加载一次,因为它在类加载时就被实例化,因此可以保证它只被实例化一次

(4)缺点

  • 加载类时就初始化,会立即占用内存空间,如果不使用它,会造成资源浪费,对整个程序加载速度有一定影响
2.懒汉式

(1)特点:

在使用时,才实例化,就延迟加载的感觉(感觉它比较懒,不用它,它就不干活)

(2)实例

  1. /**
  2. * 懒汉式
  3. * @author shixinke
  4. */
  5. public class LazySingleton {
  6. /**
  7. * 先不初始化
  8. */
  9. private static LazySingleton INSTANCE;
  10. private LazySingleton() {
  11. }
  12. /**
  13. * 在使用时初始化
  14. * @return
  15. */
  16. public static LazySingleton getInstance() {
  17. /**
  18. * 如果当前实例不存在,则new一个
  19. */
  20. if (INSTANCE == null) {
  21. INSTANCE = new LazySingleton();
  22. }
  23. return INSTANCE;
  24. }
  25. }

测试代码:

  1. class LazyRunnable implements Runnable {
  2. @Override
  3. public void run() {
  4. System.out.println(LazySingleton.getInstance());
  5. }
  6. }
  7. /**
  8. * 懒汉式测试
  9. * @author shixinke
  10. */
  11. public class LazySingletonTest {
  12. /**
  13. * 单线程测试
  14. */
  15. public static void singleThread() {
  16. LazySingleton ls1 = LazySingleton.getInstance();
  17. LazySingleton ls2 = LazySingleton.getInstance();
  18. System.out.println(ls1);
  19. System.out.println(ls2);
  20. }
  21. public static void main(String[] args) {
  22. //singleThread();
  23. new Thread(new LazyRunnable()).start();
  24. new Thread(new LazyRunnable()).start();
  25. new Thread(new LazyRunnable()).start();
  26. }
  27. }

测试结果:

  1. com.shixinke.practise.design.pattern.creation.singleton.LazySingleton@48df2951
  2. com.shixinke.practise.design.pattern.creation.singleton.LazySingleton@6fb104b6
  3. com.shixinke.practise.design.pattern.creation.singleton.LazySingleton@1467bfc6

(3)优点:

  • 懒加载:在类加载后,并不进行实例化,而是在使用时才实例化,在一定程度上能降低资源占用

(4)缺点:

  • 不是线程安全的,无法保证所有对外的方法生成的是同一个对象

补充:我们通过单步调试来分析这个结果:

多线程调试懒汉式单例模式

注:为什么它不是线程安全的呢?

  • 因为编译器为了进行程序优化,进行指令重排序,因此无法保证if判断在前,初始化在后
  • 因为CPU缓存问题,多线程对同一个共享变量的操作对于另外的线程有可见性问题,因此无法保证创建同一个对象
3.改进懒汉式

通过加锁的方式来保证线程安全

  1. /**
  2. * 懒汉式(加锁)
  3. * @author shixinke
  4. */
  5. public class SynchronizedLazySingleton {
  6. private static SynchronizedLazySingleton INSTANCE;
  7. private SynchronizedLazySingleton() {
  8. }
  9. public static synchronized SynchronizedLazySingleton getInstance() {
  10. if (INSTANCE == null) {
  11. INSTANCE = new SynchronizedLazySingleton();
  12. }
  13. return INSTANCE;
  14. }
  15. }

测试代码:

  1. class SynchronizedLazyRunnable implements Runnable {
  2. public void run() {
  3. System.out.println( SynchronizedLazySingleton.getInstance());
  4. }
  5. }
  6. /**
  7. * 懒汉式测试
  8. * @author shixinke
  9. */
  10. public class SynchronizedLazySingletonTest {
  11. public static void singleThread() {
  12. SynchronizedLazySingleton ls1 = SynchronizedLazySingleton.getInstance();
  13. SynchronizedLazySingleton ls2 = SynchronizedLazySingleton.getInstance();
  14. System.out.println(ls1);
  15. System.out.println(ls2);
  16. }
  17. public static void main(String[] args) {
  18. //singleThread();
  19. new Thread(new SynchronizedLazyRunnable()).start();
  20. new Thread(new SynchronizedLazyRunnable()).start();
  21. new Thread(new SynchronizedLazyRunnable()).start();
  22. }
  23. }

注:在getInstance前面添加synchronized关键词,表示添加一把内置锁

  • 优点:可保证线程安全,即多个线程也可以获取到同一对象
  • 缺点:因为加锁(synchronized是重量级锁)对性能有很大损耗,而且因为这里的锁是对整个类加锁,所以使用getInstance时,只能有一个线程访问资源
4.双重检测+懒汉式

目的:改进上面synchronized加锁的性能

  1. /**
  2. * 懒汉式
  3. * @author shixinke
  4. */
  5. public class DoubleCheckLazySingleton {
  6. private volatile static DoubleCheckLazySingleton INSTANCE;
  7. private DoubleCheckLazySingleton() {
  8. }
  9. public static DoubleCheckLazySingleton getInstance() {
  10. if (INSTANCE == null) {
  11. synchronized(DoubleCheckLazySingleton.class) {
  12. if (INSTANCE == null) {
  13. INSTANCE = new DoubleCheckLazySingleton();
  14. }
  15. return INSTANCE;
  16. }
  17. }
  18. return INSTANCE;
  19. }
  20. }

测试代码:

  1. class DoubleCheckLazyRunnable implements Runnable {
  2. public void run() {
  3. System.out.println( DoubleCheckLazySingleton.getInstance());
  4. }
  5. }
  6. /**
  7. * 双重检测测试
  8. * @author
  9. */
  10. public class DoubleCheckSingletonTest {
  11. public static void main(String[] args) {
  12. new Thread(new DoubleCheckLazyRunnable()).start();
  13. new Thread(new DoubleCheckLazyRunnable()).start();
  14. new Thread(new DoubleCheckLazyRunnable()).start();
  15. }
  16. }

分析:

  • 只有当INSTANCE为null时,才加锁进入下一步操作
  • 在锁内再次进行判断,是检测在加锁进入后这一对象是否被其他线程初始化
  • 使用volatile关键词,可以保证多线程之间的有序性和可见性,即对if判断和后面的初始化禁用重排序,另外,其他线程对INSTANCE的操作,对当前线程也是可见的
5.静态内部类

通过在静态内部类中初始化当前类的实例,也是通过JVM来保证该类只会被实例化一次

  1. /**
  2. * 内部类的方式
  3. * @author shixinke
  4. */
  5. public class InnerClassSingleton {
  6. private InnerClassSingleton() {
  7. }
  8. public static InnerClassSingleton getInstance() {
  9. return Inner.INSTANCE;
  10. }
  11. private static class Inner {
  12. private static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
  13. }
  14. }

测试代码:

  1. class InnerClassRunnable implements Runnable {
  2. public void run() {
  3. System.out.println(InnerClassSingleton.getInstance());
  4. }
  5. }
  6. /**
  7. * 内部类测试
  8. * @author shixinke
  9. */
  10. public class InnerClassSingletonTest {
  11. public static void singleThread() {
  12. InnerClassSingleton ls1 = InnerClassSingleton.getInstance();
  13. InnerClassSingleton ls2 = InnerClassSingleton.getInstance();
  14. System.out.println(ls1);
  15. System.out.println(ls2);
  16. }
  17. public static void main(String[] args) {
  18. //singleThread();
  19. new Thread(new InnerClassRunnable()).start();
  20. new Thread(new InnerClassRunnable()).start();
  21. new Thread(new InnerClassRunnable()).start();
  22. }
  23. }
6.枚举类

这是最推荐使用的单例设计模式实现方式

  1. public enum EnumInstance {
  2. INSTANCE;
  3. EnumInstance() {
  4. }
  5. public static EnumInstance getInstance() {
  6. return INSTANCE;
  7. }
  8. }

测试代码:

  1. class EnumSingletonRunnable implements Runnable {
  2. public void run() {
  3. System.out.println( EnumInstance.getInstance());
  4. }
  5. }
  6. /**
  7. * 枚举测试
  8. * @author
  9. */
  10. public class EnumInstanceTest {
  11. public static void main(String[] args) {
  12. new Thread(new EnumSingletonRunnable()).start();
  13. new Thread(new EnumSingletonRunnable()).start();
  14. new Thread(new EnumSingletonRunnable()).start();
  15. }
  16. }

补充:
Q:为什么枚举类单例是线程安全的?

A:这里借助反编译工具jad来查看反编译编译后的enum源码

  • jad官方网站: https://varaneckas.com/jad/
  • 下载安装: 直接下载解压即可(根据自己的操作系统选择对应的版本),把jad所在目录放入到环境变量PATH中
  • 反编译.class文件
  1. cd /data/program/github/degisn-pattern-practise/target/classes/com/shixinke/practise/design/pattern/content/creation/singleton
  2. jad EnumInstance.class
  • 查看生成的反编译后的文件(与类名同名的文件,并以.jad为后缀)
  1. // Decompiled by Jad v1.5.8e. Copyright 2001 Pavel Kouznetsov.
  2. // Jad home page: http://www.geocities.com/kpdus/jad.html
  3. // Decompiler options: packimports(3)
  4. // Source File Name: EnumInstance.java
  5. package com.shixinke.practise.design.pattern.content.creation.singleton;
  6. //它是一个final的class,因此它不能被继承
  7. public final class EnumInstance extends Enum
  8. {
  9. public static EnumInstance[] values()
  10. {
  11. return (EnumInstance[])$VALUES.clone();
  12. }
  13. public static EnumInstance valueOf(String name)
  14. {
  15. return (EnumInstance)Enum.valueOf(com/shixinke/practise/design/pattern/content/creation/singleton/EnumInstance, name);
  16. }
  17. private EnumInstance(String s, int i)
  18. {
  19. super(s, i);
  20. }
  21. public static EnumInstance getInstance()
  22. {
  23. return INSTANCE;
  24. }
  25. //INSTANCE是final的,因此也无法改变它
  26. public static final EnumInstance INSTANCE;
  27. private static final EnumInstance $VALUES[];
  28. //在static中实例化(JVM保证了它只会被实例化一次)
  29. static
  30. {
  31. INSTANCE = new EnumInstance("INSTANCE", 0);
  32. $VALUES = (new EnumInstance[] {
  33. INSTANCE
  34. });
  35. }
  36. }
7.容器类
  • 一个容器,来保存所有的实例
  • 容器对外有两个方法:
    • 注册方法:将实例对象注册到容器中(一般在初始化时注册)
    • 获取实例方法:从容器中取出实例
  1. /**
  2. * 容器类型的单例
  3. * @author shixinke
  4. */
  5. public class ContainerSingleton {
  6. /**
  7. * 保存实例的容器
  8. */
  9. private static Map<String, Object> INSTANCE_MAP = new ConcurrentHashMap<String, Object>(10);
  10. private ContainerSingleton() {
  11. }
  12. /**
  13. * 添加instance到容器
  14. * @param key
  15. * @param instance
  16. */
  17. public static void register(String key, Object instance) {
  18. if (key != null && instance != null) {
  19. if (!INSTANCE_MAP.containsKey(key)) {
  20. INSTANCE_MAP.put(key, instance);
  21. }
  22. }
  23. }
  24. /**
  25. * 取instance
  26. * @param key
  27. * @return
  28. */
  29. public static Object getInstance(String key) {
  30. return INSTANCE_MAP.get(key);
  31. }
  32. }

测试代码:

  1. package com.shixinke.practise.design.pattern.content.creation.singleton;
  2. class RegisterSingletonRunnable implements Runnable {
  3. private String key;
  4. private Object value;
  5. public RegisterSingletonRunnable(String key, Object value) {
  6. this.key = key;
  7. this.value = value;
  8. }
  9. public void run() {
  10. ContainerSingleton.register(this.key, this.value);
  11. }
  12. }
  13. class GetSingletonRunnable implements Runnable {
  14. public void run() {
  15. System.out.println( ContainerSingleton.getInstance("object"));
  16. }
  17. }
  18. /**
  19. * 容器单例测试类
  20. * @author shixinke
  21. */
  22. public class ContainerSingletonTest {
  23. public static void main(String[] args) {
  24. new Thread(new RegisterSingletonRunnable("object", new Object())).start();
  25. new Thread(new RegisterSingletonRunnable("object", new Object())).start();
  26. new Thread(new RegisterSingletonRunnable("object2", new Object())).start();
  27. new Thread(new GetSingletonRunnable()).start();
  28. new Thread(new GetSingletonRunnable()).start();
  29. new Thread(new GetSingletonRunnable()).start();
  30. }
  31. }
8总结

单例模式实现方式有多种,推荐顺序如下:

枚举类单例模式 > 静态内部类单例模式 > 饿汉式单例模式

  • 枚举类单例模式不用考虑序列化和反射来破坏单例模式的情况
  • 其他实现方式需要考虑序列化和反射破坏单例模式的情况

三.源码中单例模式的使用

1.JDK中单例模式的使用
(1)Runtime类
  1. public class Runtime {
  2. private static Runtime currentRuntime = new Runtime();
  3. public static Runtime getRuntime() {
  4. return currentRuntime;
  5. }
  6. private Runtime() {}
  7. }
  • Runtime类使用的是懒汉式的单例模式
2.Mybatis中单例模式的使用

注:ErrorContext 只保证在线程内部,实例对象是唯一的(因为使用了ThreadLocal,这个是每个线程都有一个)

  1. public class ErrorContext {
  2. private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal();
  3. private ErrorContext() {
  4. }
  5. //对外提供的获取实例的方法
  6. public static ErrorContext instance() {
  7. ErrorContext context = (ErrorContext)LOCAL.get();
  8. if (context == null) {
  9. context = new ErrorContext();
  10. LOCAL.set(context);
  11. }
  12. return context;
  13. }
  14. }