Статьи

Учебник по параллелизму Java — Видимость между потоками

При разделении состояния объекта между различными потоками, в игру вступают другие вопросы, кроме атомарности . Одним из них является видимость.

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

В многопоточной среде программа несет ответственность за определение того, когда данные распределяются между различными потоками, и действует в результате (используя синхронизацию).

Пример в NoVisibility состоит из двух потоков, которые совместно используют флаг. Поток записи обновляет флаг, а поток чтения ждет, пока флаг не будет установлен:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
public class NoVisibility {
    private static boolean ready;
     
    public static void main(String[] args) throws InterruptedException {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    if (ready) {
                        System.out.println("Reader Thread - Flag change received. Finishing thread.");
                        break;
                    }
                }
            }
        }).start();
         
        Thread.sleep(3000);
        System.out.println("Writer thread - Changing flag...");
        ready = true;
    }
}

Эта программа может привести к бесконечному циклу, поскольку поток чтения может не видеть обновленный флаг и ждать вечно.

noVisibility

Благодаря синхронизации мы можем гарантировать, что переупорядочение не произойдет, избегая бесконечного цикла. Для обеспечения видимости у нас есть два варианта:

  • Блокировка: гарантирует видимость и атомарность (при условии, что используется та же самая блокировка).
  • Изменчивое поле: гарантирует видимость.

Ключевое слово volatile действует как некий синхронизированный блок. Каждый раз, когда к полю обращаются, это похоже на вход в синхронизированный блок. Основное отличие состоит в том, что он не использует замки. По этой причине он может подходить для примеров, подобных приведенному выше (обновление общего флага), но не при использовании составных действий.

Теперь мы изменим предыдущий пример, добавив ключевое слово volatile в поле готовности.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
public class Visibility {
    private static volatile boolean ready;
     
    public static void main(String[] args) throws InterruptedException {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    if (ready) {
                        System.out.println("Reader Thread - Flag change received. Finishing thread.");
                        break;
                    }
                }
            }
        }).start();
         
        Thread.sleep(3000);
        System.out.println("Writer thread - Changing flag...");
        ready = true;
    }
}

Видимость больше не приведет к бесконечному циклу. Обновления, сделанные потоком писателя, будут видны потоку читателя:

Writer thread - Changing flag... Reader Thread - Flag change received. Finishing thread.

Writer thread - Changing flag... Reader Thread - Flag change received. Finishing thread.

Вывод

Мы узнали о другом риске при совместном использовании данных в многопоточных программах. Для простого примера, подобного показанному здесь, мы можем просто использовать изменяемое поле. Другие ситуации потребуют от нас использования атомарных переменных или блокировки.

  • Вы можете взглянуть на исходный код на github .