我试图故意创建线程的可见性问题,但得到了意外的结果:
public class DownloadStatus {
private int totalBytes;
private boolean isDone;
public void increment() {
totalBytes++;
}
public int getTotalBytes() {
return totalBytes;
}
public boolean isDone() {
return isDone;
}
public void done() {
isDone = true;
}
}
public class DownloadFileTask implements Runnable {
DownloadStatus status;
public DownloadFileTask(DownloadStatus status) {
this.status = status;
}
@Override
public void run() {
System.out.println("start download");
for (int i = 0; i < 10_000; i++) { //"download" a 10,000 bytes file each time you run
status.increment(); //each byte downloaded - update the status
}
System.out.println("download ended with: " + status.getTotalBytes()); //**NOTE THIS LINE**
status.done();
}
}
//creating threads, one to download, another to wait for the download to be done.
public static void main(String[] args) {
DownloadStatus status = new DownloadStatus();
Thread t1 = new Thread(new DownloadFileTask(status));
Thread t2 = new Thread(() -> {
while (!status.isDone()) {}
System.out.println("DONE!!");
});
t1.start();
t2.start();
}
因此,运行此命令将导致可见性问题-第二个线程将看不到更新后的值,因为它在第一个线程写回之前已将其缓存了-这会导致无休止的(while)循环,第二个线程正在不断检查缓存的isDone()。 (至少我是这样认为的)。
我没有得到的是为什么当我从调用
status.getTotalBytes()
的第二个代码块中注释掉该行时,为什么不再出现这种可见性问题。根据我的理解,两个线程都是按现状缓存状态对象,因此第二个线程应不断检查其缓存的值(并且基本上看不到第一个线程更新的新值)。
为什么此行在状态对象中调用方法会导致此可见性问题? (更有趣的是-为什么不调用它来解决?)
最佳答案
您所谓的“可见性问题”实际上是一场数据竞赛。
单个线程按它们的写入顺序查看其操作的效果。也就是说,如果您先更新变量然后读取它,您将始终在该线程中看到更新后的值。
从另一个线程查看时,线程执行的效果可能有所不同。这主要与语言和底层硬件体系结构有关。编译器可以对指令进行重新排序,在将值保留在寄存器中的同时延迟存储器写入,或者可以在将值写入主存储器之前将其保留在高速缓存中。没有显式的内存屏障,主内存中的值将不会更新。这就是您所说的“可见性问题”。System.println
中可能存在内存障碍。因此,当您执行该行时,到该点为止的所有更新都将提交给主内存,其他线程可以看到它。请注意,没有显式同步,仍然不能保证其他线程会看到它,因为这些线程可能会重复使用之前为该变量获取的值。程序中没有任何内容告诉编译器/运行时其他线程可能更改了这些值。