Java кэширует целые объекты или только части объектов? (Проблемы с видимостью) - PullRequest
0 голосов
/ 19 марта 2020

Я пытался преднамеренно создать проблемы видимости с потоками, и я получил неожиданные результаты:

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();
}

Итак, выполнение этого создаст проблему видимости - второй поток не будет посмотрите обновленное значение, так как оно кэшировало его до того, как оно было записано первым потоком - это вызывает бесконечное (пока) l oop, второй поток постоянно проверяет кэшированный isDone (). (по крайней мере, я так думаю, что это работает).

Я не понимаю, почему эта проблема с видимостью перестает возникать, когда я закомментирую строку из второго блока кода, который вызывает status.getTotalBytes(). Насколько я понимаю, оба потока начинают с кэширования объекта состояния как есть, поэтому второй поток должен постоянно проверять свое кэшированное значение (и, по сути, не видеть новое значение, обновленное первым потоком).

Почему эта строка вызывая метод в объекте статуса, вызывающий эту проблему видимости? (и что еще интереснее - почему бы не позвонить, чтобы это исправить?)

Ответы [ 2 ]

1 голос
/ 19 марта 2020

То, что вы называете «проблемой видимости», на самом деле является гонкой данных.

Один поток видит результаты своих операций в том порядке, в котором они написаны. То есть, если вы обновите переменную, а затем прочитаете ее, вы всегда увидите обновленное значение в этом потоке.

Эффекты выполнения потока могут отличаться при просмотре из другого потока. В основном это связано с языком и базовой аппаратной архитектурой. Компилятор может переупорядочивать инструкции, задерживать запись в память, сохраняя значения в регистрах, или значения могут храниться в кэше перед записью в основную память. Без явного барьера памяти значение в основной памяти не будет обновлено. Это то, что вы называете «проблемой видимости».

Вероятно, в System.println существует барьер памяти. Поэтому, когда вы выполняете эту строку, все обновления до этой точки будут зафиксированы в основной памяти, и другие потоки смогут ее увидеть. Обратите внимание, что без явной синхронизации все еще нет гарантии, что другие потоки увидят ее, потому что эти потоки могут повторно использовать значение, которое они получили для этой переменной ранее. В программе нет ничего, что сообщало бы компилятору / среде выполнения о том, что значения могут быть изменены другими потоками.

0 голосов
/ 19 марта 2020

Это состояние гонки между двумя потоками. В вашем коде нет ничего общего с оператором status.getTotalBytes(). Именно планировщик решает, какой поток будет запущен. Это случайно, что вы не застряли в бесконечном l oop после комментирования оператора println. Основная проблема в вашем коде в том, что инкремент и установка статуса должны выполняться с помощью atomi c и заменить определение метода запуска, как показано ниже. Во-вторых, инкремент также не является атомом c. Вы можете получить непредсказуемые результаты, если нет правильной синхронизации.

@Override
    public void run() {
        System.out.println("start download");
        incrementAndSetStatus();
    }

    public synchronized  void  incrementAndSetStatus(){
        for (int i = 0; i < 100000; 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();
    }
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...