IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> 单例模式还能这样写?让Android源码教你结合场景使用单例模式 -> 正文阅读

[移动开发]单例模式还能这样写?让Android源码教你结合场景使用单例模式

单例模式还能这样写?让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;
  }
  
  // Other fields and methods, all fields are final
}
  
class Helper {
  private static Instance instance = null;
  
  public static Instance getHelper() {
    Instance tem = instance;       // Only unsynchronized read of helper
    if (tem== null) {
      synchronized (Helper.class) {
        tem = instance;          // In synchronized block, so this is safe
        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

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-08-14 14:11:29  更:2021-08-14 14:13:03 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/19 1:06:03-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码