概述
- 先说下单例模式的实现,单例模式一般有 饿汉式、懒汉式、双重锁校验锁、静态内部类、枚举。
- 对于饿汉式,由于在类加载时期已经创建好单利对象,所以有且仅有一个对象,也不用考虑在多线程情况下会不会创建多个单利对象的问题。而对于懒汉式,由于延迟加载的原因,有可能导致实例多个对象等…
- 单例模式中,有四大原则,下面也就通过这四大原则来探讨下面各种单例模式。
- 构造私有。
- 以静态方法或者枚举返回实例。
- 确保实例只有一个,尤其是多线程环境。
- 确保反序列换时不会重新构建对象。
1. 饿汉式
class SingleTon {
private static SingleTon instance = new SingleTon();
//私有化构造器
private SingleTon() {
}
public static SingleTon getInstance() {
return instance;
}
}
- 在类加载时期就创建好了单例对象,在后续使用中只会使用该对象,不存在多线程下的安全问题。
- 优点: 在获取单例对象时,由于之前在类加载时期已经实例化好了,节省时间。
- 缺点:由于在类加载时期创建对象,故在占用空间。
2. 传统懒汉式
class SingleTon {
/**
* 在类加载时期不进行创建单例对象,在第一次使用时在进项创建。
*/
private static SingleTon instance = null;
private SingleTon() {
}
public static SingleTon getInstance() {
//第一次使用instance对象为null,故应该创建单利对象
if (instance == null) {
instance = new SingleTon();
}
return instance;
}
}
- 在类加载时期不进行创建单例对象,而在第一次使用时在进项创建。
- 优点: 在不使用时,不创建对象,节省空间
- 缺点 1: 在第一次使用时创建对象,导致第一个使用的人时间边长(这都不是事)
- 缺点 2: 在多线程情况下不安全,获取到的可能不是同一个对象。(严重问题)
下面对懒汉式多线程下不安全问题进行测试:
//在getInstance()方法中if判断中sleep 1毫秒
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"获取到的是\t"+ SingleTon.getInstance().hashCode());
}, "线程" + i).start();
}
}
结果:
线程1获取到的是 1958819842
线程2获取到的是 785527010
线程3获取到的是 1027487819
线程4获取到的是 1699630862
线程0获取到的是 196683177
线程5获取到的是 196683177
线程6获取到的是 196683177
线程7获取到的是 196683177
线程8获取到的是 196683177
线程9获取到的是 196683177
- 很容易发现前五个就不是同一个对象
- 那出现这个问题的原因是啥呢??? 线程1进入 if 语句,还未来的及赋值,已经有其他线程也进入,等线程1赋值完毕,其它线程已经错过了判断为空的条件,它还在 if 中 所以 其他已经进入if语句中的线程又进行了一次创建和赋值。
3. 双重校验锁(Double Check Lock)
- 先不说双重校验锁是个什么,先解决一下上面的问题。
- 懒汉式由于多线程条件下可能会有多个线程同时进入 if 语句块 并进行实例化,那么解决它可不可以使用 synchronized 同步方法,只允许一个线程进入该方法呢,答案是完全可以!!
- 代码:
public synchronized static SingleTon getInstance() {
if (instance == null) {
instance = new SingleTon();
}
return instance;
}
- 加了synchronized 完全可以解决多线程下的问题,因为有且仅有一个线程可以进入
- 但是就因为第一创建的不安全,锁住整个方法,导致在赋值后还要进行单线程得进入方法获取
- 这个方法也不是个好方法。所以 我们可以进行判断 如果已经赋值了就直接返回,没有赋值再进入同步代码块进行赋值。
- 代码:
public static SingleTon getInstance() {
if (instance == null) {
synchronized (SingleTon.class) {
instance = new SingleTon();
}
}
return instance;
}
- 这样的话还是有问题,比如 线程1,线程2,线程3 获取时这个对象还没创建出来,都是null,都同时进入if代码块中,在synchronized代码块中这三个线程依次执行,就初始化了三次。
- 所以,双重校验锁来啦
- 在 synchronized 代码块中再判断一次 instance 是否为空即可,为空才创建,不为空直接跳过去过去就可以了
class SingleTon {
private static SingleTon instance = null;
private SingleTon() {
}
public static SingleTon getInstance() {
if (instance == null) {
synchronized (SingleTon.class) {
if(instance==null){
instance = new SingleTon();
}
}
}
return instance;
}
}
- 再次测试下:
线程0获取到的是 901590997
线程2获取到的是 901590997
线程8获取到的是 901590997
线程1获取到的是 901590997
线程9获取到的是 901590997
线程3获取到的是 901590997
线程5获取到的是 901590997
线程4获取到的是 901590997
线程6获取到的是 901590997
线程7获取到的是 901590997
- 没毛病
- 但是!!! 看起来没问题,但是还是有多线程下不安全因素存在。
- 是因为在 instance = new SingleTon(); 这一句,JVM执行时是分解为三步来完成。
① 在堆内存开辟内存空间。
② 在堆内存中实例化SingleTon里面的各个参数。
③ 把对象指向堆内存空间。 - 这三个步骤并没有任何的 数据依赖 关秀,所以这三个是可被重排序的,所以真正执行的可能是 ③ 比 ①②先执行。
- 这样的执行顺序的话 一个线程先只想了③,然后被挂起了,另外一个线程进来,由于第一个线程已经执行了③,所以instance 已经不为null了,所以它直接返回,这样它就拿到的是一个未初始化完成的对象。
- 显然这样是不行的,那怎么解决,可以使用 volatile 修饰禁止指令重排序!!!
- 下面是完整版的 DCL 单例模式代码
class SingleTon {
// 使用 volatile 修饰禁止指令重排序,防止因为重排序导致的线程拿到没有初始化完成的对象
private static volatile SingleTon instance = null;
private SingleTon() {
}
public static SingleTon getInstance() {
//第一层校验: 是为了将锁的粒度化小,只是让还没有创建完成的进入去创建对象,已经创建完成的直接获取即可
if (instance == null) {
synchronized (SingleTon.class) {
//第二次校验:当多个线程一起进入上一层 if 判读,使用第二层可只让一个线程去实例化
if(instance==null){
instance = new SingleTon();
}
}
}
return instance;
}
}
- 要是还进行优化的话,可以尝试一下下面这个
4. 静态内部类实现单例模式
class SingleTon {
private SingleTon() {
}
//静态内部类
private static class SingleTonHoler {
private static SingleTon instance = new SingleTon();
}
public static SingleTon getInstance() {
return SingleTonHoler.instance;
}
}
要想弄明白静态内部类单例,先介绍个东西,类的加载时机:
- 类加载时机:JAVA虚拟机在有且仅有的5种场景下会对类进行初始化。
- 遇到new、getstatic、setstatic或者invokestatic这4个字节码指令时,对应的java代码场景为:new一个关键字或者一个实例化对象时、读取或设置一个静态字段时 (final修饰、已在编译期把结果放入常量池的除外)、调用一个类的静态方法时。
- 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没进行初始化,需要先调用其初始化方法进行初始化。
- 当初始化一个类时,如果其父类还未进行初始化,会先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类。
- 当使用JDK 1.7等动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
- 这5种情况被称为是类的主动引用,注意,这里《虚拟机规范》中使用的限定词是"有且仅有",那么,除此之外的所有引用类都不会对类进行初始化,称为被动引用。静态内部类就属于被动引用的行列。
- 外部类加载时并不需要立即加载 内部类,内部类不被加载也就不会去初始化 instance,这样实现了懒汉式。
- 即当SingleTon第一次被加载时,并不需要去加载SingleTonHoler,只有当getInstance()方法第一次被调用时,才会去初始化instance。
- 调用getInstance()方法会导致虚拟机加载SingleTonHoler类,加载过程中初始化静态变量。
那么,它是怎么保证多线程情境下的安全性的呢??
- 虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕。
- 如果在一个类的()方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行()方法后,其他线程唤醒之后不会再次进入()方法。同一个加载器下,一个类型只会初始化一次。),在实际应用中,这种阻塞往往是很隐蔽的。
- 故而,可以看出INSTANCE在创建过程中是线程安全的,所以说静态内部类形式的单例可保证线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
那么,是不是可以说静态内部类单例就是最完美的单例模式了呢?
- 其实不然,静态内部类也有着一个致命的缺点,就是传参的问题,由于是静态内部类的形式去创建单例的,故外部无法传递参数进去,例如Context这种参数,所以,我们创建单例时,可以在静态内部类与DCL模式里自己斟酌。
而且,在序列化和反序列化后返回的不是同一个对象(严重!!!!)
public static void main(String[] args) {
try {
SingleTon serialize = SingleTon.getInstance();
System.out.println(serialize.hashCode());
//序列化
FileOutputStream fo = new FileOutputStream("tem");
ObjectOutputStream oo = new ObjectOutputStream(fo);
oo.writeObject(serialize);
oo.close();
fo.close();
//反序列化
FileInputStream fi = new FileInputStream("tem");
ObjectInputStream oi = new ObjectInputStream(fi);
SingleTon serialize2 = (SingleTon) oi.readObject();
oi.close();
fi.close();
System.out.println(serialize2.hashCode());
} catch (Exception e) {
e.printStackTrace();
}
}
static class SingleTon implements Serializable {
private SingleTon() {
}
//静态内部类
private static class SingleTonHoler {
private static SingleTon instance = new SingleTon();
}
public static SingleTon getInstance() {
return SingleTonHoler.instance;
}
}
输出:
1956725890
931919113
- 这显然就不是一个对象,解决方法添加readResolve方法,告诉反射,在获取对象时调用哪个方法。
class SingleTon implements Serializable {
private SingleTon() {
}
//静态内部类
private static class SingleTonHoler {
private static SingleTon instance = new SingleTon();
}
public static SingleTon getInstance() {
return SingleTonHoler.instance;
}
//使用匿名内部类实现单例模式,在遇见序列化和反序列化的场景,得到的不是同一个实例
//解决这个问题是在序列化的时候使用readResolve方法
protected Object readResolve() {
return SingleTonHoler.instance;
}
}
5. 枚举实现
public enum SingleTon{
INSTANCE;
}