以下代码永远不会打印Done
。但是,如果我取消注释System.out.println(list.size());
,它将结束。为什么会这样?
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.LinkedBlockingQueue;
public class Test3 {
static BlockingQueue<String> blockingQueue;
static List<String> list = new ArrayList<>();
static void run() {
while (true) {
try {
String s = blockingQueue.take();
Thread.sleep(1000);
list.add(s);
}catch (InterruptedException e){}
}
}
public static void main(String[] args) {
blockingQueue = new LinkedBlockingQueue<>();
blockingQueue.add("test1");
blockingQueue.add("test2");
blockingQueue.add("test3");
CompletableFuture.runAsync(() -> run());
while (list.size() < 3) {
// uncomment the line below to make it finish
//System.out.println(list.size());
}
System.out.println("Done");
}
}
我似乎没有任何想法。
答案 0 :(得分:2)
编译器可以自由优化
while (list.size() < 3) {
// uncomment the line below to make it finish
//System.out.println(list.size());
}
到
if (list.size() < 3) {
while(1);
}
为什么?因为这看起来像是CPU正在从缓存中读取列表的大小,并且允许CPU具有缓存并从中读取。
请注意,这并不是说CPU实际上有缓存,也不是实际上从缓存中读取了任何东西。只是允许CPU具有缓存并从中读取内容,而您的代码必须考虑到这一点,而您做不到。
Java提供了可用于解决此问题的任意数量的同步原语。
对于一种语言来说,这似乎是一个奇怪的决定。但是,仅在少数情况下才需要跨线程同步,并且通过假设不需要跨线程同步而允许的优化数量巨大且意义重大。因此,责任由要求同步的代码承担。
当您听到用将缓存刷新到主存储器的方式描述了Java的存储器语义时,请理解这是一个抽象。系统的行为就像一样,它具有需要刷新的缓存。同步功能的行为就好像它们将数据刷新到主存储器或从主存储器中读取数据一样。但是,在大多数系统中,实际发生的情况是完全不同的,因为可以在您可能在其上运行Java代码的大多数现实多核CPU上的硬件中保证CPU缓存一致性。
这有点有趣,但是这种神话般的缓存和刷新使代码更加有效,即使它在现实的现代硬件中实际上并不存在!要求程序员的行为就好像存在这样的缓存并且需要这种刷新一样,这一事实允许对代码生成进行优化,从而显着提高性能。
答案 1 :(得分:0)
根据:https://docs.oracle.com/javase/7/docs/api/java/util/ArrayList.html ArrayList不是线程安全的,引用:
请注意,此实现未同步。如果有多个线程 同时访问ArrayList实例,并且至少一个 线程会在结构上修改列表,因此必须同步 在外部。 (结构修改是指添加或 删除一个或多个元素,或显式调整后备数组的大小; 仅设置元素的值不是结构性的 修改。)通常是通过同步一些 自然封装列表的对象。如果不存在这样的对象, 该列表应使用Collections.synchronizedList“包装” 方法。
要修复使用情况,请执行以下操作:
static List<String> list = java.util.Collections.synchronizedList(new ArrayList());