定义与类型

定义:保证一个类仅有一个实例,并提供一个全局访问点

类型:创建型

UML

单例模式的基本要素

  • 私有的构造方法

  • 指向自己实例的私有静态引用

  • 以自己实例为返回值的静态的公有的方法

适用场景

  • 像确保任何情况下都绝对只有一个实例

  • 需要频繁实例化然后销毁的对象。

  • 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。

  • 有状态的工具类对象。

  • 频繁访问数据库或文件的对象。

优点

  • 在内存里只有一个实例,减少了内存开销

  • 可以避免对资源的多重占用

  • 避免重复创建对象,提高性能

  • 设置全局访问点,严格控制访问

缺点

  • 没有接口,扩展困难

  • 违反开闭原则

单例模式的几种实现方式

1.饿汉式

饿汉式:顾名思义,对象比较饥饿,所以一开始就创建好了。饿汉式也是单例模式的最简单实现。

/** 饿汉式 一开始就new好了 */
public class HungrySingleton implements Serializable {
​
    /** 可以直接new也可以适用静态块中创建 */
    private final static HungrySingleton hungrySingleton;
​
    static {
        hungrySingleton = new HungrySingleton1();
    }
​
    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }
    /**
     * 私有构造函数
     */
    private HungrySingleton() {}
}
  • 饿汉式的单例模式,对象一开始就创建好了。不需要考虑线程安全问题。

  • 饿汉式单例模式如果消耗资源比较多,而对象未被适用则会造成资源浪费。

2.懒汉式

懒汉式:说明类对象比较懒,没有直接创建,而是延迟加载的,是第一次获取对象的时候才创建。懒汉式的单例模式应用较多。

A、第一个版本的Java实现(非线程安全)

/** 懒汉式 线程不安全 */
public class LazySingleton {
​
    private static LazySingleton lazySingleton = null;
​
    //线程不安全,当有两个线程同时创建对象,会违背单例模式
    public static LazySingleton getInstance() {
        if (lazySingleton == null) {
            //会发生指令重排
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
    private LazySingleton() {}
}
  • 这个版本的懒汉式会出现线程安全的问题,当两个线程同时访问getInstance()静态方法时,lazySingleton还未创建,就会创建出两个实例,违背了单例模式。

  • 这里可以在getInstance()方法添加同步锁synchronized解决,也可以在方法体添加类锁,但是这样相当于完全锁住了getInstance(),会出现性能问题。

  • 推荐适用下面这种方式

B、双重检查锁double check懒汉式(线程安全,通常适用这种方式)

/** 懒汉式 线程安全 */
public class LazyDoubleCheckSingleton {
​
    //volatile 禁止指令重排序
    private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
​
    /** 在静态方法中直接加synchronized相当于锁了类 */
    public static LazyDoubleCheckSingleton getInstance() {
        //同样实锁类, 指令重排序
        if (lazyDoubleCheckSingleton == null) {
            synchronized (LazyDoubleCheckSingleton.class) {
                if (lazyDoubleCheckSingleton == null) {
                    /**
                     * 1.分配内存给这个对象
                     * 2.初始化对象
                     * 3.设置lazyDoubleCheckSingleton指向刚分配的内存
                     * 2 3 顺序有可能发生颠倒
                     * intra-thread semantics 不会改变单线程执行结果,指令重排序
                     */
                    lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
                }
            }
        }
        return lazyDoubleCheckSingleton;
    }
    private LazyDoubleCheckSingleton() {}
}
  • 双重检查,只有对象为空的时候才会需要同步锁,而第二次判断是否为null,是对象是否已经创建。

  • 添加volatile关键字,防止指令重排序。

C、基于静态内部类的延迟加载方案

  • 私有静态类的延迟加载

  • 将延迟初始化交给静态类的初始化

public class StaticInnerClassSingleton {
​
    /**
     * 看静态类的初始化锁那个线程可以拿到
     */
    private static class InnerClass {
        private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
    }
​
    public static StaticInnerClassSingleton getInstance() {
        return InnerClass.staticInnerClassSingleton;
    }
​
    private StaticInnerClassSingleton () {
        if (InnerClass.staticInnerClassSingleton != null) {
            throw new RuntimeException("单例对象禁止反射调用");
        }
    }
}

3.容器单例

  • 使用静态容器方式来实现多单例类

public class ContainerSingleton {
​
    //静态容器, 注意map不是线程安全的,如果为了线程安全可以使用HashTable或者ConcurrentHashMap
    private static Map<String, Object> singletonMap = new HashMap<>();
​
    public static void putInstance (String key, Object instance) {
        if (key != null && key.length() != 0) {
            if (!singletonMap.containsKey(key)) {
                singletonMap.put(key, instance);
            }
        }
    }
​
    public static Object getInstance (String key) {
        return singletonMap.get(key);
    }
}
  • 容器单例如果要保证线程安全性,建议使用ConcurrentHashMap

  • 通常使用容器单例情况是:单例对象比较多,需要统一维护。

4、枚举单例模式(推荐使用)

  • 枚举单例是从JVM层面上做的限制

public enum EnumInstance {
​
    /**
     * 具体的单例实例
     */
    INSTANCE {
        protected void  printTest () {
            System.out.println("K.O print Test!");
        }
    };
    private Object data;
    protected abstract void printTest();
    public Object getData() {
        return data;
    }
    public void setData(Object data) {
        this.data = data;
    }
    public static EnumInstance getInstance() {
        return INSTANCE;
    }
}
  • 单例模式完美防御了反射与序列化攻击

5、ThreadLocal线程单例(并不是严格意义上的单例模式)

  • 有一部分场景,要求对象的生命周期随着线程

/**
 * 线程级单例模式
 */
public class ThreadLocalInstance {
​
    //静态的ThreadLocal类保存对象
    private static final ThreadLocal<ThreadLocalInstance> threadLocal =
            ThreadLocal.withInitial(ThreadLocalInstance::new);
​
    private ThreadLocalInstance () {}
​
    public static ThreadLocalInstance getInstance () {
        return threadLocal.get();
    }
}

单例模式关注的重点

  • 1、私有构造器

  • 2、线程安全

  • 3、延迟加载

  • 4、序列化和反序列化安全

  • 5、反射攻击安全

相关设计模式

  • 单例模式和工厂模式:工厂类可以设计成单例模式。

  • 单例模式和享元模式:可以通过享元模式来获取单例对象