单例模式还能这样写?让Android源码教你结合场景使用单例模式
伟大的海贼王哥尔?D?罗杰曾经说过:“去看源码吧!我把世上的一切都放在了那里。” 众所周知,单例模式常用的有DCL、静态内部类、枚举等等,静态内部类方法可以实现延迟加载还没有线程安全问题,枚举方法可以从JVM层面防止反射破坏单例模式,实际上我们还可以使用final来实现单例模式,这个后面再说。而DCL(Double Check Lock)是一种线程安全、延迟加载、效率高的懒汉式单例模式,大概长下面这样。
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;
}
}`
其实就是缩小了锁的粒度来提高性能,然后为了防止指令重排序导致对象半初始化问题使用了volatile来修饰对象。但是我在看Android的WindowManagerGlobal源码的时候发现Google开发团队是这样实现单例模式的:
private static WindowManagerGlobal sDefaultWindowManager;
private WindowManagerGlobal() {
}
public static WindowManagerGlobal getInstance() {
synchronized (WindowManagerGlobal.class) {
if (sDefaultWindowManager == null) {
sDefaultWindowManager = new WindowManagerGlobal();
}
return sDefaultWindowManager;
}
}
嗯?居然少了一层if判断,这样子在对象已经存在的时候还要去获取锁不是让效率变低了吗?连volatile也不加,难道他们不担心会出现对象半初始化的问题吗?
带着上面两个问题,我去看了看getInstance方法调用的地方,只有两处,一处就在类里面,另一处在WindowManager的实现类WindowManagerImpl中。而他是这样用的:
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
新的问题产生了,为什么要用final修饰一个单例对象?我们知道final修饰对象的时候是可以保证对象不可变,但是单例的对象不是已经不可变了吗?看过《java并发编程艺术》应该知道,final还有一个重要的语义,那就是可以添加内存屏障。 volatile也有内存屏障,难道这里是使用了final来代替volatile来实现单例模式吗? 我们先来看一个普通的饿汉式单例:
public class Singleton{
private static final Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
很多人不理解这里的final作用到底是什么,好像加不加都一样。其实这里的作用是通过final的内存屏障来保证对象在初始化未完成的时候就被其他线程获取也就是上面提到的对象半初始化问题。 这是网上的一种错误观点。 我认为这里的final加不加都行,因为static保证了对象是在类加载的“初始化”阶段就完成创建了。
再来看看真正用final实现单例模式是什么样子的:
public final class Instance {
private final int n;
public Instance(int n) {
this.n = n;
}
}
class Helper {
private static Instance instance = null;
public static Instance getHelper() {
Instance tem = instance;
if (tem== null) {
synchronized (Helper.class) {
tem = instance;
if (tem == null) {
tem = new Instance(42);
instance = tem;
}
}
}
return tem;
}
}
这种写法初始化的对象暂时存在一个局部变量 tem 中,在 tem 初始化完成并赋值给instance之前,其他线程来访问instance都会是null。final的作用就是防止tem还没初始化完成就赋值给了instance。想了解具体原理可以自行去看看final域的读写规则。
再对比一下WindowManagerGlobal的单例模式就知道其实这里并没有用final代替volatile的作用。
但是我们可以发现要实现线程安全的单例模式,其实只要控制对于对象非同步访问就可以了。比如上面final的实现方法,对于对象instance的非同步访问只有把instance赋值给局部变量tem一个地方,然后把对于instance的赋值操作放进了同步代码块里面。 我们再看一次WindowManagerGlobal的单例模式:
private static WindowManagerGlobal sDefaultWindowManager;
private WindowManagerGlobal() {
}
public static WindowManagerGlobal getInstance() {
synchronized (WindowManagerGlobal.class) {
if (sDefaultWindowManager == null) {
sDefaultWindowManager = new WindowManagerGlobal();
}
return sDefaultWindowManager;
}
}
应该很多人都能看出来,少了一层 if 判空其实就不会有指令重排序导致的对象半初始化的问题,也就不需要用volatile修饰对象了。究其原因,就是把对象的访问都放进同步代码块中,就算发生了指令重排序,其他线程依旧需要获取锁,等获取到时对象也已经初始化完成了。 安全问题解决了,我们再来看看效率问题。乍一看每次都要去获取锁,获取不到又要一直等啊等的,肯定效率下降很多吧?
我对于这个问题的理解是这样的: 首先这种写法没有用volatile修饰变量,不用每次都去主内存更新数据,提高了一部分效率。 其次就是使用场景,WindowManagerGlobal对象一般是在对window进行添加、更新、移除操作的时候使用的,还有就是可见性发生变化的时候,而这些操作很少同时执行,就算同时执行也一般发生在主线程。也就是说锁竞争的情况非常少,而锁竞争不激烈的时候synchronized的锁就会是无锁或者偏向锁,所以在主线程获取锁的时候可以直接获取锁或者让系统对比一下线程信息再获取,不会对效率有什么影响。所以这种方式下平时主线程获取对象的效率会比使用volatile的方式要高。 至于在WindowManagerImpl用final修饰单例对象,我只能理解为防止被重新赋值。 如果你有其他想法,欢迎在评论区发表。 参考文章: 一种独特的单例模式写法,利用final语义实现
LCK10-J. Use a correct form of the double-checked locking idiom
|