写点什么

volatile 关键字在 Android 中到底有什么用?,零基础入门 android

用户头像
Android架构
关注
发布于: 23 小时前

}}}


static class Thread2 extends Thread {@Overridepublic void run() {while (true) {if (!flag) {flag = true;System.out.println("Thread2 set flag to true");}}}}


}


这段代码真的非常简单,我们开启了两个线程来对同一个变量 flag 进行修改。Thread1 使用一个 while(true)循环,发现 flag 是 true 时就把它改为 false。Thread2 也使用一个 while(true)循环,发现 flag 是 false 时就把它改为 true。


理论上来说,这两个线程同时运行,那么就应该一直交替打印,你改我的值,我再给你改回去。


实际上真的会是这样吗?我们来运行一下就知道了。



可以看到,打印过程只持续了一小会就停止打印了,但是程序却没有结束,依然显示在运行中。


这怎么可能呢?理论上来说,flag 要么为 true,要么为 false。true 的时候 Thread1 应该打印,false 的时候 Thread2 应该打印,两边都不打印是为什么呢?


我们用刚才所学的知识就可以解释这个原本解释不了的问题,因为 Thread1 和 Thread2 的 CPU 高速缓存中各有一份 flag 值,其中 Thread1 中缓存的 flag 值是 false,Thread2 中缓存的 flag 值是 true,所以两边就都不会打印了。


这样我们就通过一个实际的例子演示了刚才所说的可见性问题。那么该如何解决呢?


答案很明显,volatile。


volatile 这个关键字的其中一个重要作用就是解决可见性问题,即保证当一个线程修改了某个变量之后,该变量对于另外一个线程是立即可见的。


至于 volatile 的工作原理,太底层方面的内容我也说不上来,大概原理就是当一个变量被声明成 volatile 之后,任何一个线程对它进行修改,都会让所有其他 CPU 高速缓存中的值过期,这样其他线程就必须去内存中重新获取最新的值,也就解决了可见性的问题。


我们可以将刚才的代码进行如下修改:


public class Main {


volatile static boolean flag;...


}


没错,就是这么简单,在 flag 变量的前面加上 volatile 关键字即可。然后重新运行程序,效果如下图所示。



一切如我们所预期的那样运行了。

指令重排

《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


问题


volatile 关键字还有另外一个重要的作用,就是禁止指令重排,这又是一个非常有趣的问题。


我们先来看两段代码:


// 第一段代码 int a = 10;int b = 5;a = 20;System.out.println(a + b);


// 第二段代码 int a = 10;a = 20;int b = 5;System.out.println(a + b);


第一段代码,我们声明了一个 a 变量等于 10,又声明了一个 b 变量等于 5,然后将 a 变量的值改成了 20,最后打印 a + b 的值。


第二段代码,我们声明了一个 a 变量等于 10,然后将 a 变量的值改成了 20,又声明了一个 b 变量等于 5,最后打印 a + b 的值。


这两段代码有区别吗?


不用瞎猜了,这两段代码没有任何区别,声明变量 b 和修改变量 a 之间的顺序是随意的,它们之间谁也不碍着谁。


也正是因为这个原因,CPU 在执行代码时,其实并不一定会严格按照我们编写的顺序去执行,而是可能会考虑一些效率方面的原因,对那些先后顺序无关紧要的代码进行重新排序,这个操作就被称为指令重排。


这么看来,指令重排这个操作没毛病啊。确实,但只限在单线程环境下。


很多问题一旦进入了多线程环境,就会变得更加复杂,我们来看如下代码:


public class Main {


static boolean init;static String value;


static class Thread1 extends Thread {@Overridepublic void run() {value = "hello world";init = true;}}


static class Thread2 extends Thread {@Overridepublic void run() {while (!init) {// 等待初始化完成}value.toUpperCase();}}


}


这段代码的思路仍然很简单,Thread1 用于对 value 数据进行初始化,初始化完成之后会将 init 设置成 true。Thread2 则会先通过 while 循环等待初始化完成,完成之后再对 value 数据进行操作。


那么这段代码可以正常工作吗?未必,因为根据刚才的指令重排理论,Thread1 中 value 和 init 这两个变量之间是没有先后顺序的。如果 CPU 将这两条指令进行了重排,那么就可能出现初始化已完成,但是 value 还没有赋值的情况。这样 Thread2 的 while 循环就会跳出,然后在操作 value 的时候出现空指针异常。


所以说,指令重排功能一旦进入了多线程环境,也是可能会出现问题的。


而至于解决方案嘛,当然还是 volatile 了。


对某个变量声明了 volatile 关键字之后,同时也就意味着禁止对该变量进行指令重排。所以我们只需要这样修改代码就能够保证程序的安全性了。


public class Main {


volatile static boolean init;...


}

volatile 在 Android 上的应用

现在我们已经了解了 volatile 关键字的主要作用,但是就像开篇时那位朋友提到的一样,很多人想不出来这个关键字在 Android 上有什么用途。


其实我觉得任何一个技术点都不应该去生搬硬套,你只要掌握了它,该用到时能想到它就可以了,而不是绞尽脑汁去想我到底要在哪里使用它。


我在看一些 Google 库的源码时,其实时不时就能看到这个关键字,只要是涉及多线程编程的时候,volatile 的出场率还是不低的。


这里我给大家举一个常见的示例吧,在 Android 上我们应该都编写过文件下载这个功能。在执行下载任务时,我们需要开启一个线程,然后从网络上读取流数据,并写入到本地,重复执行这个过程,直到所有数据都读取完毕。


那么这个过程我可以用如下简易代码进行表示:


public class DownloadTask {


public void download() {new Thread(new Runnable() {@Overridepublic void run() {while (true) {byte[] bytes = readBytesFromNetwork(); // 从网络上读取数据 if (bytes.length == 0) {break; // 下载完毕,跳出循环}writeBytesToDisk(bytes); // 将数据写入到本地}}}).start();}


}


到此为止没什么问题。


不过现在又来了一个新的需求,要求允许用户取消下载。我们都知道,Java 的线程是不可以中断的,所以如果想要做取消下载的功能,一般都是通过标记位来实现的,代码如下所示:


public class DownloadTask {


boolean isCanceled = false;


public void download() {new Thread(new Runnable() {@Overridepublic void run() {while (!isCanceled) {byte[] bytes = readBytesFromNetwork();if (bytes.length == 0) {break;}writeBytesToDisk(bytes);

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
volatile关键字在Android中到底有什么用?,零基础入门android