1.什么叫单例模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
2.先来个非线程安全的例子
不就是一个类一个对象吗,so esay。
先把构造函数私有化了,再实现个getInstance方法(判断对象是否存在,存在则返回,不存在则创建),齐活。
public
当然这个例子的缺点也是显而易见的,它并不是线程安全的,如果多个线程同时调用getInstance方法,可能会创建多个Singleton对象。
3.如果要求线程安全呢?
好吧好吧,要求这么高,线程安全的也是手到擒来,再加个双重检查锁定(double-checked locking),完美!
public
如果你真的这么想,那很不幸,你写了一段java官方认定的“臭名昭著的”代码,为什么官方要这么认定呢?下面来一探究竟:
首先,这段代码的核心INSTANCE = new Singleton()不是原子操作,这段代码可以简单分为下面三步执行:
1. 为 INSTANCE对象在堆内存(逻辑上为方法区)中分配空间;
2. 初始化 INSTANCE对象;
3. 将 INSTANCE变量指向分配的堆内存(逻辑上为方法区)地址
由于但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。
例如,线程 A 执行了 1 和 3,此时 线程 B 调用 getInstance() 后发现 INSTANCE 变量不为空,因此返回 INSTANCE对象,但此时 INSTANCE对象还未被初始化,线程B就获得了一个未完全初始化的INSTANCE对象。
有的同学会说,我加了synchronized关键字了啊,synchronized不是可以保证有序性吗?
synchronized所能保证的有序性是相对的,它只能保证synchronized块与synchronized块之间的有序性,而synchronized块里面的非原子操作依然可能发生指令重排序。
那我们应该怎么解决这个问题呢,Java中还有一个叫volatile的关键字可以保证可见性和有序性,那用volatile修饰INSTANCE可以吗?
答案是可以的,volatile 关键字提供内存屏障的方式来防止指令被重排,编译器在生成字节码文件时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
内存屏障会确保指令重排序时,不会把操作volatile关键字修饰对象指令后面的指令排到内存屏障之前的位置,也不会把操作volatile关键字修饰对象指令前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。
经使用volatile 关键字更正后的代码如下所示:
public
但Java官方文档中仍不建议这样做,虽然volatile 关键字可以解决双重检查锁定(double-checked locking)带来的问题,但是volatile 关键字的写性能比较差,会拉低代码的整体性能。
那有没有更好的实现方法呢?当然有,如下所示:
public
也可以通过静态内部类来实现:
public
上面两个实现,都没使用synchronized,那是如何保证线程安全的呢?
这两种方法都利用了ClassLoader的线程安全机制,ClassLoader的loadClass方法在加载类的时候使用了synchronized关键字,确保多线程情况下也只有一个INSTANCE能够成功被初始化。
4.不直接或间接使用锁能否实现线程安全呢?
当然可以,使用CAS来实现一下,由于INSTANCE对象只会被赋值一次,所以也不会面临CAS中需要增加额外版本号才可以解决的ABA问题:
public
CAS缺点在于如果循环执行一直不成功,CPU开销会很大,执行过程中可能会创建多个Singleton对象(可被回收,最终还是只剩一个Singleton对象)
5.还有没有其他方式实现线程安全呢?
ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离多个线程对数据的访问冲突,同步机制采用时间换空间,ThreadLocal采用空间换时间。
public
6.结语
单例模式的实现方式非常多,除了上述5种,单例模式还可以使用枚举来实现,那枚举实现的单例是线程安全的吗?又是如何保证线程安全的?