DMOZ中文网站分类目录-免费收录各类优秀网站的中文网站目录.
  • DmozDir.org
DMOZ目录快速登录入口-免费收录各类优秀网站的中文网站目录.由人工编辑,并提供网站分类目录检索及地区分类目录检索,是站长免费推广网站的有力平台!

volatile关键字在Android中到底有什么用?

  • volatile关键字在Android中到底有什么用?

  • 已被浏览: 13 次2020年10月15日    来源:  https://blog.csdn.net/sinyu890807/article/details/109009649
  • 本文同步发表于我的微信公众号 扫一扫文章底部的二维码或在微信搜索 郭霖 即可关注 每个工作日都有文章更新。 上周六在公众号分享了一篇关于Java volatile关键字的文章 发布之后有朋友在留言里指出 说这个关键字没啥用啊 Andr

    本文同步发表于我的微信公众号,扫一扫文章底部的二维码或在微信搜索 郭霖 即可关注,每个工作日都有文章更新。

    上周六在公众号分享了一篇关于Java volatile关键字的文章,发布之后有朋友在留言里指出,说这个关键字没啥用啊,Android开发又不像服务器那样有那么高的并发,老分享这种知识干啥?

    让我意识到有些朋友对于volatile这个关键字的理解还是有误区的。

    另外也有朋友留言说,虽然知道volatile关键字的作用,但是想不出在Android开发中具体有什么用途。

    所以我准备写篇文章来剖析一下这个关键字,顺便回答一下这些朋友的疑问。

    由于这篇文章是我用周日一天时间赶出来的,所以可能不会像平时的文章那样充实,但是对于上述问题我相信还是可以解释清楚的。

    对volatile关键字的作用有疑问的同学,可能都不太了解CPU高速缓存这个概念,所以我们先从这个概念讲起。


    CPU高速缓存和可见性问题

    当一个程序运行的时候,数据是保存在内存当中的,但是执行程序这个工作却是由CPU完成的。那么当CPU正在执行着任务呢,突然需要用到某个数据,它就会从内存中去读取这个数据,得到了数据之后再继续向下执行任务。

    这是理论上理想的工作方式,但是却存在着一个问题。我们知道,CPU的发展是遵循摩尔定律的,每18个月左右集成电路上晶体管的数量就可以翻一倍,因此CPU的速度只会变得越来越快。

    但是光CPU快没有用呀,因为CPU再快还是要从内存去读取数据,而这个过程是非常缓慢的,所以就大大限制了CPU的发展。

    为了解决这个问题,CPU厂商引入了高速缓存功能。内存里存储的数据,CPU高速缓存里也可以存一份,这样当频繁需要去访问某个数据时就不需要重复从内存中去获取了,CPU高速缓存里有,那么直接拿缓存中的数据即可,这样就可以大大提升CPU的工作效率。

    而当程序要对某个数据进行修改时,也可以先修改高速缓存中的数据,因为这样会非常快,等运算结束之后,再将缓存中的数据写回到内存当中即可。

    这种工作方式在单线程的场景下是没问题的,准确来讲,在单核多线程的场景下也是没问题的。但如果到了多核多线程的场景下,可能就会出现问题。

    我们都知道,现在不管是手机还是电脑,动不动就声称是多核的,多核就是CPU中有多个运算单元的意思。因为一个运算单元在同一时间其实只能处理一个任务,即使我们开了多个线程,对于单核CPU而言,它只能先处理这个线程中的一些任务,然后暂停下来转去处理另外一个线程中的任务,以此交替。而多核CPU的话,则可以允许在同一时间处理多个任务,这样效率当然就更高了。

    但是多核CPU又带来了一个新的挑战,那就是在多线程的场景下,CPU高速缓存中的数据可能不准确了。原因也很简单,我们通过下面这张图来理解一下。

    可以看到,这里有两个线程,分别通过两个CPU的运算单元来执行程序,但它们是共享同一个内存的。现在CPU1从内存中读取数据A,并写入高速缓存,CPU2也从内存中读取数据A,并写入高速缓存。

    到目前为止还是没有问题的,但是如果线程2修改了数据A的值,首先CPU2会更新高速缓存中A的值,然后再将它写回到内存当中。这个时候,线程1再访问数据A,CPU1发现高速缓存当中有A的值啊,那么直接返回缓存中的值不就行了。此时你会发现,线程1和线程2访问同一个数据A,得到的值却不一样了。

    这就是多核多线程场景下遇到的可见性问题,因为当一个线程去修改某个变量的值时,该变量对于另外一个线程并不是立即可见的。

    为了让以上理论知识更具有说服力,这里我编写了一个小Demo来验证上述说法,代码如下所示:

    public class Main {
    
        static boolean flag;
    
        public static void main(String... args) {
            new Thread1().start();
            new Thread2().start();
        }
    
        static class Thread1 extends Thread {
            @Override
            public void run() {
                while (true) {
                    if (flag) {
                        flag = false;
                        System.out.println("Thread1 set flag to false");
                    }
                }
            }
        }
    
        static class Thread2 extends Thread {
            @Override
            public 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关键字即可。然后重新运行程序,效果如下图所示。

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


    指令重排问题

    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 {
            @Override
            public void run() {
                value = "hello world";
                init = true;
            }
        }
    
        static class Thread2 extends Thread {
            @Override
            public 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() {
                @Override
                public 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() {
                @Override
                public void run() {
                    while (!isCanceled) {
                        byte[] bytes = readBytesFromNetwork();
                        if (bytes.length == 0) {
                            break;
                        }
                        writeBytesToDisk(bytes);
                    }
                }
            }).start();
        }
    
        public void cancel() {
            isCanceled = true;
        }
    
    }
    

    这里我们增加了一个isCanceled变量和一个cancel()方法,调用cancel()方法时将isCanceled变量设置为true,表示下载已取消。

    然后在download()方法当中,如果发现isCanceled变量为true,就跳出循环不再继续执行下载任务,这样也就实现了取消下载的功能。

    这种写法能够正常工作吗?根据我的实际测试,确实基本上都是可以正常工作的。

    但是这种写法真的安全吗?不,因为你会发现download()方法和cancel()方法是运行在两个线程当中的,因此cancel()方法对于isCanceled变量的修改,未必对download()方法就立即可见。

    所以,存在着这样一种可能,就是我们明明已经将isCanceled变量设置成了true,但是download()方法所使用的CPU高速缓存中记录的isCanceled变量还是false,从而导致下载无法被取消的情况出现。

    因此,最安全的写法就是对isCanceled变量声明volatile关键字:

    public class DownloadTask {
    
        volatile boolean isCanceled = false;
        ...
    
    }
    

    这样就可以保证你的取消下载功能始终是安全的了。

    好了,关于volatile关键字的作用,以及它在Android开发中具体有哪些用途,相信到这里就解释的差不多了。

    本来是想用周日一天时间写篇小短文的,写着写着好像最后又写出了不少内容,不过只要对大家有帮助就好。



    如果想要学习Kotlin和最新的Android知识,可以参考我的新书 《第一行代码 第3版》,点击此处查看详情。


    关注我的技术公众号,每个工作日都有优质技术文章推送。

    微信扫一扫下方二维码即可关注:


    以上信息来源于网络,如有侵权,请联系站长删除。

    TAG:什么用 关键字 volatile

  • 上一篇:学习vue
  • 与“volatile关键字在Android中到底有什么用?”相关的资讯
  • 6种方法的JavaScript绑定此关键字的反应,ES6和ES7
  • volatile型变量语义讲解一 :对所有线程的可见性
  • jQuery的关键字标签后
  • sql注入里关键字绕过的发现
  • 关键字研究要点第2部分:如何选择合适的关键字