UI 阻塞循环行为不同(Oreo 和 Marshmallow)

UI blocking loops behaviours differ( Oreo vs Mashmallow)

我有一个小型 Android 应用程序,它执行服务器调用 post 一些用户数据到服务器。 以下是代码:

private boolean completed = false;
public String postData( Data data){

    new Thread(new Runnable() {
        @Override
        public void run() {

            try{

                String response = callApi(data);

                completed = true;



            }catch(Exception e){

                Log.e("API Error",e.getMessage());
                completed = true;
                return;
            }


        }
    }).start();

    while(!completed){

  //      Log.i("Inside loop","yes");
    }

    return response.toString();
}

上述方法调用 API 到 post 数据和 return 收到的响应,效果很好。 底部的循环是一个 UI 阻塞循环,它阻塞 UI 直到收到响应或错误。

问题:

我为 Marshmallow 和 Oreo 设备尝试了相同的代码,但结果不同。

对于 Marshmallow : 事情的发展符合我的预期。 :)

对于奥利奥 (8.1.0):

打开应用程序后,第一个 API 调用效果很好。然而,随后的 API 调用导致 UI 永远阻塞,尽管从服务器接收到错误或响应(通过日志记录和调试验证)。

但是,在设置断点时(运行 在 Debug 模式下)应用程序运行起来要容易得多。

虽然满足条件,但系统似乎无法退出UI阻塞循环。

注意到的第二个行为是当我在 UI 阻塞线程中记录一条消息时,系统能够通过 [=36= 从方法中退出循环和 return ] 响应未记录。

有人可以帮助理解 Android 这两种口味之间的这种不一致,引入的变化可能会导致奥利奥出现这种行为,而棉花糖不会出现这种情况吗? 任何见解都会非常有帮助。

It's more likely to be differences in the processor cache implementation in the two different hardware devices you're using. Probably not the JVM at all.

内存一致性是一个相当复杂的话题,我建议您查看 this for a more in-depth treatment. Also see this java memory model explainer 之类的教程,了解 JVM 将提供的保证的详细信息,无论您的硬件如何。

我将解释一个假设的场景,在这个场景中,您观察到的行为可能会发生,而不知道您的芯片组的具体细节:

假设情景

两个线程:您的 "UI thread"(假设它在核心 1 上 运行ning)和 "background thread"(核心 2)。您的变量 completed 在编译时被分配了一个固定的内存位置(假设我们已经取消引用 this,等等,并且我们已经确定了那个位置)。 completed用单字节表示,初始值为“0”。

内核 1 上的 UI 线程很快进入忙等待循环。第一次尝试读取 completed 时,有一个 "cache miss"。因此,请求通过 通过 缓存,并从主内存中读取 completed (连同 cache line 中的其他 31 个字节)。现在cache line在core 1的L1 cache中,读取值,发现是“0”。 (核心不直接连接到主内存;它们只能通过缓存访问它。)所以忙等待继续;核心 1 一次又一次地请求相同的内存位置 completed,但不是高速缓存未命中,L1 现在能够满足每个请求,并且不再需要与主内存通信。

与此同时,在核心 2 上,后台线程正在努力完成 API 调用。最终它完成,并尝试将“1”写入相同的内存位置 completed。同样,存在高速缓存未命中,并且会发生同样的事情。核心 2 将“1”写入其自己的 L1 缓存中的适当位置。但是该缓存行不一定会写回主内存。即使有,核心 1 也没有引用主内存,所以它不会看到变化。核心 2 然后完成线程,returns,然后去其他地方工作。

(当核心 2 被分配给另一个进程时,它的缓存可能已经同步到主内存并刷新。因此,“1”确实回到了主内存。但这并不是说与核心 1 的区别,核心 1 继续 运行 完全从其 L1 高速缓存。)

事情以这种方式继续,直到发生某些事情提示核心 1 的缓存已脏,需要刷新。正如我在评论中提到的,这可能是 fence 作为 System.out.println() 调用、调试器入口等的一部分发生。自然地,如果您使用了 synchronized 块,编译器会在您自己的代码中设置了栅栏。

要点

...这就是为什么您总是使用 synchronized 块来保护对共享变量的访问! (所以你不必花几天时间阅读处理器手册,试图了解你正在使用的特定硬件上的内存模型的细节,只是为了在两个线程之间共享一个字节的信息。)一个 volatile 关键字也将解决问题,但请参阅 Jenkov 文章中的一些链接,了解其中的不足之处。