本文探讨一下线程安全下单单例模式, 参考了多篇文章, 结合自己的一些经验.
饿汉模式
饿汉模式本身是一种提前创建好实例的方式, 利用jvm 在加载类的时候, 只会加载一次, 以及加载类本身jvm是通过加锁保证线程安全的, 因此饿汉模式将实例作为类的静态变量, 就实现了线程安全的单例.
写法
public class Singleton {
// 静态变量 在类加载后就实例化完成
private static Singleton instance = new Singleton();
// 构造方法私有化 防止直接通过类创建实例
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
复制代码
缺点
- (1) 类在加载时就创建好了实例变量, 啥时候用就不确定了, 存在浪费资源的可能性
- (2) 无法解决序列化后, 再次反序列时, 无法保证单例了
- (3) 无法避免反射的攻击
懒汉
懒汉模式就是说再第一次需要单例对象的时候, 再完成单例对象的生成, 最优写法就是利用synchronized关键字 + volatile实现双重校验.
写法
public class Singleton {
private static volatile Singleton instance;
// 构造方法私有化 防止直接通过类创建实例
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
// 第一次使用, 保证线程安全
synchronized (Singleton.class) {
// 并发情况下 可能有多个线程阻塞在上面一行
// 某个线程完成创建后, 多个线程会陆续进入到下面一行, 为了避免重复生成
// 需要二次校验
if (instance == null) {
instance = new Singleton();
}
}
}
// 如果已经创建好 直接返回
return instance;
}
}
复制代码
缺点
(1) 依旧是反序列化的问题, 如果你序列化一个单例类的对象,接下来复原多个那个对象,那你就会有多个单例类的实例。
(2) 无法避免反射的攻击
静态内部类
在单例类中创建一个静态内部类, 这样单例类在被创建的时候, 静态内部类不会完成类的加载, 只有当真正使用到静态内部类的时候, 才会去创建相应的类对象, 完成对单例对象的加载, 实现了懒加载.
Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显式装载SingletonHolder类
写法
public class Singleton {
// 构造方法私有化 防止直接通过类创建实例
private Singleton() {
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
private static class SingletonHolder {
// 在静态内部类中完成实例的创建
// 即当真正使用到SingletonHolder这个类的时候, 才会去加载SingletonHolder类对象
// 那么自然这个时候才去创建相应的静态变量, 实现了延迟加载的功能
private static final Singleton instance = new Singleton();
}
}
复制代码
缺点
(1) 依旧存在反序列化的问题.
(2) 无法避免反射的攻击
枚举
枚举实现单例, 有两种方式,
方式1
这种方式是将枚举作为内部类来使用, 毕竟枚举也是一个类, 因此枚举类也具有延迟加载的能力, 不同的是, 枚举类是通过jvm会保证枚举类的构造函数只会执行一次这个特点, 来保证单例的.
写法
public class Singleton {
// 构造方法私有化 防止直接通过类创建实例
private Singleton() {
}
public static Singleton getInstance() {
return SinletonHolder.INSTANCE.getInstance();
}
// 枚举类
private enum SinletonHolder {
INSTANCE;
private Singleton instance;
// jvm会保证枚举类的构造函数只被执行一次
SinletonHolder() {
instance = new Singleton();
}
// 枚举类里的方法需要通过枚举值进行调用
private Singleton getInstance() {
return instance;
}
}
}
复制代码
方式2
第二种方式, 就是直接将枚举类认为是我们的单例的类,
写法
public enum Singleton {
INSTANCE;
public void doSomething() {
// 你想要通过单例做什么, 就可以在这里面实现
}
}
复制代码
缺点
首先强调下, 这两种写法功能性上没什么差别, 都是通过枚举类的特点来保证单例和线程安全
(1) 没啥缺点, 这种方式是Effective Java作者Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,可谓是很坚强的壁垒啊.
(2) 也解决了反射攻击的问题
序列化问题
为啥普通方式单例对象反序列化时会创建新对象?
任何一个readObject方法,不管是显式的还是默认的,它都会返回一个新建的实例,这个新建的实例不同于该类初始化时创建的实例。”
当然,这个问题也是可以解决的,可以通过对序列化机制进行定制, 主要就是readResolve方法
枚举类为啥没有反序列化的问题?
在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。
同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。
反射问题
普通类方式实现枚举为啥有反射问题?
根据java的反射, 获取到类的class对象后, 可以利用它的构造函数创建新的实例, 即使它的构造函数是私有的, 依然可以调用
普通单例模式如何解决反射问题?
可以在私有的构造函数中判断一下属性是不是null, 如果不是null, 就抛出异常.
public class Singleton {
private static Singleton instance;
private Singleton() {
if (instance != null) {
// 这样就避免了反射攻击的问题
throw new RuntimeException();
}
}
}
复制代码
枚举为啥没有反射问题?
反射在通过newInstance创建对象时,会检查该类是否ENUM修饰,如果是则抛出异常,反射失败。
利用CAS实现单例模式
如果我不想使用synchronized关键字来实现单例模式呢? 即使是类加载保证的线程安全, 底层也是加了锁的, 那么就可以采用CAS实现.
CAS的好处在于不需要使用传统的锁机制来保证线程安全,CAS是一种基于忙等待的算法,依赖底层硬件的实现,相对于锁它没有线程切换和阻塞的额外消耗,可以支持较大的并行度。
写法
public class Singleton {
private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<Singleton>();
private Singleton() {}
public static Singleton getInstance() {
for (;;) {
Singleton singleton = INSTANCE.get();
if (null != singleton) {
return singleton;
}
// CAS操作
singleton = new Singleton();
if (INSTANCE.compareAndSet(null, singleton)) {
return singleton;
}
}
}
}
复制代码
缺点
(1) 如果忙等待一直执行不成功(一直在死循环中),会对CPU造成较大的执行开销。