Многопоточное состояние Java в общем массиве символов

Предположим, у меня есть следующее:

char[] shared = new char[]{'a', 'b'};
ExecutorService exec = Executors.newCachedThreadPool();
Future<?> f1 = exec.submit(() -> shared[0] = 'A');
Future<?> f2 = exec.submit(() -> shared[1] = 'B');
f1.get(); f2.get();
System.out.println(shared);

Я ожидаю, что это будет обычно печатать:

'AB'

Но возможно ли это для печати:

'aB'

или же

'bA'

Мне интересно, если с shared не является энергозависимым, один поток может, сохраняя свои изменения в соответствующем элементе char, сохранить соседние байты, которые устарели в кеше локального процессора, и растоптать изменения, сделанные другим потоком. У меня есть ощущение, что ответом будет "нет", потому что я думаю, что это нарушило бы модель памяти Java, которая гласит, что всякий раз, когда поток присоединяется, все действия, которые он выполняет, выполняются до соединения. Но если это так, то мне интересно, как это предотвратить? Насколько я понимаю, когда байты сбрасываются, вся строка кэша сбрасывается, и я предполагаю, что массив char[] будет иметь несколько элементов, расположенных в одной строке кэша.

2 ответа

Нет, это невозможно. Вы можете убедиться в этом сами, если посмотрите на сгенерированный байт-код.

// lambda synthetic method
private static synthetic lambda$main$0([C)Ljava/lang/Character; throws java/lang/Exception
    L0
    LINENUMBER 11 L0
    ALOAD 0           // Loads reference of array "shared"
    ICONST_0          // Load index 0 onto the stack
    BIPUSH 65         // 65 = letter A in unicode
    DUP_X2            
    CASTORE           // Consumes reference, index, and value from stack
    INVOKESTATIC java/lang/Character.valueOf (C)Ljava/lang/Character;
    ARETURN
    MAXSTACK = 4
    MAXLOCALS = 1

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

Это было точно указано в спецификации языка Java:

17.6. Разрывая слово

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

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

В худшем случае это будет означать выполнение таких обновлений массива с помощью инструкции CAS или аналогичной, чтобы гарантировать отсутствие промежуточного обновления. Но, насколько мне известно, архитектура x86/x64 не подвержена этому влиянию, так как в ней есть инструкции по изменению элементов меньшего размера, и она будет обрабатывать аннулирование строки кэша другого ядра ЦП, чтобы другие ядра ЦП извлекли измененные данные перед выполнением собственного обновления. Конечно, это все еще медленнее, чем два ядра ЦП, выполняющие обновления на разных строках кэша.

Другие вопросы по тегам