我试图故意创建线程的可见性问题,但得到了意外的结果:

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中可能存在内存障碍。因此,当您执行该行时,到该点为止的所有更新都将提交给主内存,其他线程可以看到它。请注意,没有显式同步,仍然不能保证其他线程会看到它,因为这些线程可能会重复使用之前为该变量获取的值。程序中没有任何内容告诉编译器/运行时其他线程可能更改了这些值。

07-28 01:11