Статьи

Модель памяти Java и оптимизация

обзор

Многие разработчики многопоточного кода знакомы с идеей, что разные потоки могут иметь различное представление значения, которое они содержат, и это не единственная причина, по которой поток может не увидеть изменения, если он не сделан потокобезопасным. Сам JIT может сыграть свою роль.

Почему разные темы видят разные значения?

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

локальная копия, например, в кеше 1-го уровня. Этот кеш, как правило, в конечном итоге соответствует. Я видел короткие периоды от одной микросекунды до 10 миллисекунд, когда два потока видят разные значения. В конце концов поток переключается по контексту, кэш очищается или обновляется. Нет никаких гарантий относительно того, когда это произойдет, но это почти всегда намного меньше секунды.

Как JIT может сыграть свою роль?

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

Пример

Этот код будет работать, пока логическое значение не будет установлено в false.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
>static class MyTask implements Runnable {
    private final int loopTimes;
    private boolean running = true;
    boolean stopped = false;
 
    public MyTask(int loopTimes) {
        this.loopTimes = loopTimes;
    }
 
    @Override
    public void run() {
        try {
            while (running) {
                longCalculation();
            }
        } finally {
            stopped = true;
        }
    }
 
    private void longCalculation() {
        for (int i = 1; i < loopTimes; i++)
            if (Math.log10(i) < 0)
                throw new AssertionError();
    }
}
 
public static void main(String... args) throws InterruptedException {
    int loopTimes = Integer.parseInt(args[0]);
    MyTask task = new MyTask(loopTimes);
    Thread thread = new Thread(task);
    thread.setDaemon(true);
    thread.start();
    TimeUnit.MILLISECONDS.sleep(100);
    task.running = false;
    for (int i = 0; i < 200; i++) {
        TimeUnit.MILLISECONDS.sleep(500);
        System.out.println("stopped = " + task.stopped);
        if (task.stopped)
            break;
    }
}

Этот код многократно выполняет некоторую работу, которая не влияет на память. Единственное отличие, которое это делает, это то, сколько времени это займет. Принимая больше времени, он определяет, будет ли оптимизирован код в run () до или после запуска установлено значение false.

Если я запускаю это с 10 или 100 и -XX: + PrintCompilation я вижу

01
02
03
04
05
06
07
08
09
10
11
12
13
14
111    1     java.lang.String::hashCode (55 bytes)
112    2     java.lang.String::charAt (29 bytes)
135    3     vanilla.java.perfeg.threads.OptimisationMain$MyTask :longCalculation (35 bytes)
204    1 % ! vanilla.java.perfeg.threads.OptimisationMain$MyTask :run @ 0 (31 bytes)
stopped = false
stopped = false
stopped = false
stopped = false
... many deleted ...
stopped = false
stopped = false
stopped = false
stopped = false
stopped = false

Если я запускаю это с 1000, вы можете увидеть, что run () не скомпилирована, и поток останавливается

1
2
3
4
5
112    1     java.lang.String::hashCode (55 bytes)
112    2     java.lang.String::charAt (29 bytes)
133    3     vanilla.java.perfeg.threads.OptimisationMain $MyTask::longCalculation (35 bytes)
135    1 %   vanilla.java.perfeg.threads.OptimisationMain $MyTask::longCalculation @ 2 (35 bytes)
stopped = true

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

Как это исправить

Простое решение — сделать поле нестабильным. Это гарантирует, что значение поля будет согласованным, а не только согласованным, что может сделать для вас кеш.

Вывод

Хотя есть много примеров таких вопросов, как; Почему моя нить не останавливается? Ответ имеет больше общего с моделью памяти Java, которая позволяет JIT «встроить» поля, которые он выполняет в аппаратном обеспечении, и иметь несколько копий данных в разных кэшах.

Ссылка: Java Memory Model и оптимизация от нашего партнера JCG Питера Лоури из блога Vanilla Java .