本文介绍 Java
单例模式,主要包括:
饿汉式单例实现
懒汉式单例简单实现
懒汉式线程安全加锁单例实现
懒汉式双重检查加锁单例实现。
使用枚举实现单例
饿汉式
饿汉式的特点就是空间换时间,在开始时就创建,而且只创建一次,节约了运行时间,保证了线程安全,缺点就是当你不需要用的时候也同样占用内存空间。
1 | public class TokenProvider { |
懒汉式
懒汉式的特点就是时间换空间,不需要时不创建,需要时才创建,节约了内存空间,缺点就是每次获取都需要判断,增加了运行时间,而且不加锁的懒汉式无法保证线程安全。
看一个最简单的 饿汉式 创建单例的方法
1 | public class TokenProvider { |
为什么会有线程安全的问题呢?因为多线程访问时,假设 A 线程正在创建实体,此时 B 线程已经开始进行 sInst == null
的判空操作,此时 B 线程便会创建一个新的实体。
如何解决?请看下面线程安全加锁
线程安全加锁
当使用懒汉式时,如果多线程同时创建单例,不加锁的话就会创建多个实体,无法保证线程安全,此时应在判空操作之前加锁,让其他线程在外面等待已经进入的线程完成操作,创建完成实体,这时第二个线程进入时,sInst
已经不为空可以直接返回,从而保证线程安全。
加锁保证线程安全
1 | public class TokenProvider { |
缺点也很明显,需要使用该单例时,所有线程都会首先进入同步代码块,在线程同步时会浪费很多时间,我们需要避免这种情况,请看下节双重检查加锁
双重检查加锁
为了解决上面的问题,我们需要避免每次都进行同步加锁,在最外层先进行判空操作,当实体已经创建时,后面的线程则不需要进入同步代码块等待,节约了时间。
volatile
: 被volatile
修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。
我们必须使用 volatile
来避免指令重排的问题,简单的来说在 2
处创建对象时,为 sInst
分配内存空间 和 初始化 sInst
两条指令可能发生重排序,这会导致在 1
处获取到一个没有被初始化的对象导致问题发生。Double-Check-Locking 指令重排问题
1 | public class TokenProvider { |
使用枚举实现单例
简单说一下枚举,枚举类似类,一个枚举可以拥有成员变量,成员方法,构造方法。创建enum
时,编译器会自动为我们生成一个继承自 Java.lang.Enum
的类,构建实例的过程不是我们做的,一个 enum
的构造方法限制是 private
的,也就是不允许我们调用。单例中的每一个都是 static final
类型,下面代码的对比应该更清楚一些
1 | enum Type{ |
等同于
1 | class Type extends Enum{ |
在枚举中我们明确了构造方法限制为私有,在我们访问枚举实例时会执行构造方法,同时每个枚举实例都是 static final
类型的,也就表明只能被实例化一次。在调用构造方法时,我们的单例被实例化。
也就是说,因为 enum
中的实例被保证只会被实例化一次,所以我们的 INSTANCE
也被保证实例化一次。
1 | public enum TokenProviderSingleton { |