简介
确保某一个类只有一个实例。并且自行实例化向整个系统提供这个实例。
关键点:
- 使用 private 修饰构造方法。
- 通过一个静态方法或枚举返回单例类对象。
- 确保只有一个实例对象,尤其是多线程环境下。
- 确保单例类对象在反序列化时不会重新构建对象。
饿汉式
public class SingleTon{private static SingleTon INSTANCE = new SingleTon();private SingleTon(){}public static SingleTon getInstance(){return INSTANCE;}}
饿汉模式在类被初始化时就已经在内存中创建了对象,以空间换时间,故不存在线程安全问题。
懒汉式单例演变:1. 为了解决多线程问题,进行加锁校验。—> 2. 为了避免每次获取单例都要进行加锁校验,使用双重检查锁模式。—> 3. 为了避免 JVM 重排序问题,使用 volatile 进行修饰单例。
**
懒汉式
1. 为了解决多线程问题,进行加锁校验。
这里的严重缺点就是每次获取单例类对象的时候都会进行加锁操作,消耗资源。
public class SingleInstanceDemo {private static SingleInstanceDemo INSTANCE = null;public static synchronized SingleInstanceDemo getInstance() {if (INSTANCE == null) {INSTANCE = new SingleInstanceDemo();}return INSTANCE;}private SingleInstanceDemo() {}}
2. 双重检查锁模式。
解决了每次访问都加锁的问题,只有第一次创建实例的时候加锁。
public class SingleInstanceDemo {private static SingleInstanceDemo INSTANCE = null;public static SingleInstanceDemo getInstance() {//双重检查锁校验if (INSTANCE == null) {synchronized (SingleInstanceDemo.class) {if (INSTANCE == null) {INSTANCE = new SingleInstanceDemo();}}}return INSTANCE;}private SingleInstanceDemo() {}}
这里又延伸出一个新问题:由于 JVM 的重排序问题,会导致可能先给 instance 进行赋值操作,然后再调用构造函数进行初始化。
下面是字节码指令:
17: new #3 // 创建对象,将对象的引用入栈 new Singleton()20: dup // 复制一份这个对象的引用(引用地址)21: invokespecial #4 // 利用对象的引用来调用构造方法(根据引用地址调用)24: putstatic #2 // 利用一份对象引用赋值给static Instance
jvm虚拟机在执行时有可能做优化(指令重排序优化),也就是可能先执行24,再执行21,那么会导致什么问题呢?synchronized只可能保证同步代码块内原子性(注:synchronized代码块内的代码仍可能发生有序性问题,即指令重排序),但是无法保证外面if判断。啥意思呢?
当T1线程执行到同步代码块内,发生了指令重排序,先调用了24行的指令,将对象的引用赋值给了static Instance,那么此时T2执行到同步代码块外面的if判断,就会发现Instance不为Null,就继续执行返回,可返回的时候,T1还未将构造方法初始完毕。
3. 使用 volatile 进行修饰单例
volatile 避免了 JVM 重排序问题
public class SingleInstanceDemo {private volatile static SingleInstanceDemo1 INSTANCE = null;public static SingleInstanceDemo1 getInstance() {if (INSTANCE == null) {synchronized (SingleInstanceDemo1.class) {if (INSTANCE == null) {INSTANCE = new SingleInstanceDemo1();}}}return INSTANCE;}private SingleInstanceDemo() {}}
静态内部类模式
public class SingleTon{private SingleTon(){}private static class SingleTonHoler{private static SingleTon INSTANCE = new SingleTon();}public static SingleTon getInstance(){return SingleTonHoler.INSTANCE;}}
类加载时机:JAVA虚拟机在有且仅有的5种场景下会对类进行初始化。
1.遇到new、getstatic、setstatic或者invokestatic这4个字节码指令时,对应的java代码场景为:new一个关键字或者一个实例化对象时、读取或设置一个静态字段时(final修饰、已在编译期把结果放入常量池的除外)、调用一个类的静态方法时。
2.使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没进行初始化,需要先调用其初始化方法进行初始化。
3.当初始化一个类时,如果其父类还未进行初始化,会先触发其父类的初始化。
4.当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类。
5.当使用JDK 1.7等动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
这5种情况被称为是类的主动引用,注意,这里《虚拟机规范》中使用的限定词是”有且仅有”,那么,除此之外的所有引用类都不会对类进行初始化,称为被动引用。静态内部类就属于被动引用的行列。
我们再回头看下getInstance()方法,调用的是SingleTonHoler.INSTANCE,取的是SingleTonHoler里的INSTANCE对象,跟上面那个DCL方法不同的是,getInstance()方法并没有多次去new对象,故不管多少个线程去调用getInstance()方法,取的都是同一个INSTANCE对象,而不用去重新创建。当getInstance()方法被调用时,SingleTonHoler才在SingleTon的运行时常量池里,把符号引用替换为直接引用,这时静态对象INSTANCE也真正被创建,然后再被getInstance()方法返回出去,这点同饿汉模式。那么INSTANCE在创建过程中又是如何保证线程安全的呢?在《深入理解JAVA虚拟机》中,有这么一句话:
虚拟机会保证一个类的
故而,可以看出INSTANCE在创建过程中是线程安全的,所以说静态内部类形式的单例可保证线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
那么,是不是可以说静态内部类单例就是最完美的单例模式了呢?其实不然,静态内部类也有着一个致命的缺点,就是传参的问题,由于是静态内部类的形式去创建单例的,故外部无法传递参数进去,例如Context这种参数,所以,我们创建单例时,可以在静态内部类与DCL模式里自己斟酌。
反序列化不重新构建对象
杜绝单利对象在被反序列化的时候重新生成对象,需在单例类中加入一下方法
private Object readResolve() throws ObjectStreamException{return instance;}
总结
优点:
- 避免对象的频繁创建,减少内存开销;如果一个对象需要读取配置等,使用单例也能减少系统性能的开销。
- 建立全局访问点,优化和共享资源访问。
缺点:
- 很难扩展,如果要扩展,除了在原有基础上修改代码外很难有其它途径。
- 生命周期长,容易造成内存泄漏。
Kotlin 扩展
Object 关键字实现单例的原理
定义一个 object 修饰的类
object SingleInstanceDemo2 {}
反编译后
可以看出,object 修饰的单例是饿汉式。在类加载的时候就已经创建了。
by lazy 创建的是不是单例?
不是!
上代码
class SingleInstanceDemo1 {private val INSTANCE: SingleInstanceDemo1 by lazy{SingleInstanceDemo1()}}
反编译:
创建了一个代理对象。在使用 INSTANCE 的时候,是通过 get() 方法获取的。
Lazy 是一个 接口,它有一个实现类 **SynchronizedLazyImpl**。而且 _LazyKt.lazy((Function0)null.INSTANCE);_的内部实现就是:
所以 具体看 SynchronizedLazyImpl 的 value 获取:
可以看出如果有值,就直接返回,如果没有值就加锁创建。所以,通过 by lazy 创建的字段也是线程安全的。
