我有一个小的Android应用程序,该应用程序执行服务器调用以将一些用户数据发布到服务器。 以下是代码:
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来发布数据,并返回收到的响应,效果很好。 底部的循环是一个UI阻止循环,该循环阻止UI直到收到响应或错误为止。
问题:
我为棉花糖和奥利奥设备尝试了相同的代码,结果却有所不同。
棉花糖:事情进展符合我的预期。 :)
对于奥利奥(8.1.0):
打开应用程序后,第一次API调用就足够好了。但是,随后的后续API调用将导致UI永远阻塞,尽管从服务器收到了错误或响应(已通过日志和调试验证)。
但是,在设置断点(在Debug模式下运行)时,应用程序移动的麻烦就更少了。
尽管满足条件,但似乎系统无法退出UI阻止循环。
注意到的第二个行为是,当我在UI阻止线程中记录一条消息时,尽管没有记录API响应,系统仍能够退出循环并从Method返回。
有人能帮助理解这两种Android版本之间的这种不一致吗?引入的更改可能会导致Oreo而不是棉花糖这样的行为吗? 任何见解都将非常有帮助。
答案 0 :(得分:0)
您正在使用的两种不同硬件设备中的处理器缓存实现上的差异可能更大。可能根本不是JVM。
内存一致性是一个非常复杂的主题,建议您参考this之类的教程,以进行更深入的处理。另请参阅此java memory model explainer,以获取有关JVM将提供的保证的详细信息,而与您的硬件无关。
我将解释一个假设场景,在这种情况下,您所观察到的行为可能会发生,而又不知道芯片组的具体细节:
假设情景
两个线程:您的“ UI线程”(假设它在内核1上运行)和“后台线程”(内核2)。在编译时,为变量completed
分配了一个固定的内存位置(假设我们已经取消引用this
等,并且我们已经确定了该位置)。 completed
由单个字节表示,初始值为“ 0”。
位于核心1上的UI线程快速到达busy-wait循环。第一次尝试读取completed
时,出现“缓存未命中”。因此,请求会通过缓存,并从主内存中读取completed
(以及cache line中的其他31个字节)。现在,高速缓存行位于核心1的L1高速缓存中,它读取该值,并发现其为“ 0”。 (内核没有直接连接到主内存;它们只能通过其缓存访问。)核心1一次又一次地请求相同的内存位置completed
,但是L1现在可以满足每个请求,而不再需要与主内存进行通信,而不再是高速缓存未命中。
同时,在核心2上,后台线程正在工作以完成API调用。最终,它结束,并尝试向同一存储位置completed
中写入“ 1”。再次,存在高速缓存未命中,并且发生了类似的事情。内核2将“ 1”写入其自己的L1缓存中的适当位置。但是,该缓存行并不一定要写回到主存储器。即使这样做,内核1也不会引用主内存,因此它不会看到更改。然后,Core 2完成该线程,返回,然后去其他地方工作。
(通过将核心2分配给另一个进程的时间,它的缓存可能已同步到主内存并进行了刷新。因此,“ 1”确实使其返回主内存。这并不意味着核心1,继续从其一级缓存中继续运行。)
然后事情以这种方式继续下去,直到碰巧表明核心1的缓存脏了,需要刷新为止。正如我在评论中提到的,这可能是fence发生在System.out.println()
调用,调试器条目等的一部分。自然,如果您使用过synchronized
块,则编译器会在您自己的代码中设置了围栏。
重要事项
...这就是为什么您总是使用synchronized
块来保护对共享变量的访问! (因此,您不必花几天时间阅读处理器手册,就可以在两个线程之间共享一个字节的信息,而不必了解正在使用的特定硬件上的内存模型的详细信息。)也可以解决该问题,但请参见Jenkov文章中的某些链接,以了解其中的不足之处。