什么是单例模式
- 构造函数私有
- 以静态方法或者枚举返回实例
- 确保实例只有一个,不论是在单线程环境
- 确保反序列化时候不会重新构建对象
常用的单例模式有
恶汉模式,懒汉模式,双重校验锁懒汉模式,静态内部类,枚举模式
恶汉模式
public class SingleTon {
private static SingleTon INSTANCE = new SingleTon();
private SingleTon() {
}
public static SingleTon getInstance() {
return INSTANCE;
}
}
懒汉模式在类被初始化就已经在内存中创建对象,以空间换区时间,不存在线程安全问题
懒汉模式
public class SingleTon {
private static SingleTon INSTANCE = null;
private SingleTon() {
}
public static SingleTon getInstance() {
if (INSTANCE == null) {
INSTANCE = new SingleTon();
}
}
}
懒汉模式在方法被调用后才创建对象**,以时间换取空间,在多线程环境下存在风险**)(可以理解比较懒,只有用到时候才去创建)
双重锁懒汉模式(DCL)
public 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;
}
}
}
DCL模式的优点就是,只有在对象需要被使用时才创建,第一次判断INSTANCE == null为了避免非必要加锁,当第一次加载时才对实例进行加锁再实例化,这样既可以节约内存空间,又可以保证线程安全,由于JVM存在指令重排的,所以DCL也会存在线程不安全的情况
双重锁懒汉模式(DCL)
public 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;
}
}
}
- 优点
DCL的优点就是只要对象被用到的时候才创建,第一次INSTANCE为空是为了避免非必要加锁,当第一次被加载时才对实例进行加载和实例化,这样既可以节约内存空间,有能保证线程安全,但是由于JVM存在指令重排的功能,DCL也会出现线程不安全的问题。具体分析如下
INSTANCE = new SingleTon();
这个步骤,其实在JVM里面其实执行三步,
- 在堆内存中开辟内存空间
- 在堆内存中实例化SingTon里面的各个参数
- 把对象指向堆内存空间
由于JVM存在指令重排的功能,可能2还没执行,3就已经执行了,如果恰巧此时在切换到线程B上,这个时候由于已经执行了3.判断INSTANCE为非空,直接拿来用,这样的话就会出现异常,这就是著名的DCL失效问题,
怎么解决DCL失效问题
我们通过Volatile关键字修饰变量声明,禁止指令重排来解决DCL失效问题。Volatile确保INSTANCE每次均在主内存中读取,这样虽然损失一点效率但是也是无伤大雅
静态内部类
public class SingleTon {
private SingleTon() {
}
private static class SingleTonHoler {
private static SingleTon INSTANCE = new SingleTon();
}
public static SingleTon getInstance() {
return SingleTonHoler.INSTANCE;
}
}
-
静态内部类的优点
外部加载类的时候并不需要立马加载内部类,内部类不被加载则不会初始化INSTANCE,故不占内存,既当SingleTon第一次被加载的时候,并不是立马就去加载SingleTonHoler,只要当genstance()被调用的时候,才会去初始化INSTANCE,这样不仅能保证线程安全,也能保证单例的唯一性,同时推迟了线程的初始化
-
静态内部类的缺点
静态内部类无法传递参数,由于是静态内部类的去创建单例的,故外部无法传递参数。
类加载时机,JAVA虚拟机有且仅有下列五种场景会对类进行初始化
- 遇到new,getstatic,setstatic或者invokestatic这四个字节码指令的时候,对应的java应用场景就是:new一个关键字,或者一个实例化对象时读取或者设置一个静态字段时(final修饰,已在编译期把结果放在常量池的除外),调用一个类的静态方法的时候
- 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没进行初始化,需要先调用其初始化方法进行初始化
- 当初始化一个类时,如果其父类还未进行初始化,会先触发其父类的初始化
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类
- 使用JDK 1.7等动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
这5种情况被称为是类的主动引用 (有且仅有这五种情况,JVM才会主动初始化类,除此之外,其他情况都是被动引用)
我们再来看 getInstance()方法,调用的是SingleTonHoler.INSTANCE,取的是SingleTonHoler里的INSTANCE对象,跟上面那个DCL方法不同的是,getInstance()方法并没有多次去new对象,故不管多少个线程去调用getInstance()方法,取的都是同一个INSTANCE对象,而不用去重新创建。当getInstance()方法被调用时,SingleTonHoler才在SingleTon的运行时常量池里,把符号引用替换为直接引用,这时静态对象INSTANCE也真正被创建,然后再被getInstance()方法返回出去,这点同饿汉模式 相似, 那么INSTANCE在创建过程中又是如何保证线程安全的呢
虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕。如果在一个类的()方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行()方法后,其他线程唤醒之后不会再次进入()方法。同一个加载器下,一个类型只会初始化一次。),在实际应用中,这种阻塞往往是很隐蔽的。
说明参考
文章为看视频博客学习过程中总结,方便自己以后好复习
https://www.bilibili.com/video/BV18b411M7xz
http://moguit.cn/#/
https://blog.csdn.net/mnb65482/article/details/80458571