Частичный ответ: как сегодня работает небезопасная републикация в OpenJDK.
(Это не окончательный общий ответ, который я хотел бы получить, но, по крайней мере, он показывает, чего ожидать от самой популярной реализации Java. )
Короче говоря, это зависит от того, как объект был опубликован изначально:
- если первоначальная публикация выполняется через изменчивую переменную, то небезопасная перепубликация скорее всего безопасна, т. е. вы скорее всего никогда не увидите объект как частично сконструированный
- если первоначальная публикация осуществляется через синхронизированный блок, то небезопасная перепубликация скорее всего небезопасна, т.е. вы скорее всего сможете увидеть объект как частично построенный
Скорее всего потому, что я основываю свой ответ на сборке, сгенерированной JIT для моей тестовой программы, и, поскольку я не являюсь экспертом в JIT, меня не удивит, если JIT сгенерирует совершенно другой машинный код на чужой компьютер.
Для тестов я использовал OpenJDK 64-Bit Server VM (сборка 11.0.9+11-alpine-r1, смешанный режим) на ARMv8.
ARMv8 был выбран, потому что он имеет очень упрощенная модель памяти, которая требует инструкций барьера памяти как в потоках издателя, так и в потоках чтения (в отличие от x86). .
<сильный>1. Первоначальная публикация через volatile переменную: скорее всего безопасно
Тестовая java-программа похожа на вопрос (я только добавил еще один поток, чтобы увидеть, какой код сборки генерируется для изменчивой записи):
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Fork(value = 1,
jvmArgsAppend = {"-Xmx512m", "-server", "-XX:+UnlockDiagnosticVMOptions", "-XX:+PrintAssembly",
"-XX:+PrintInterpreter", "-XX:+PrintNMethods", "-XX:+PrintNativeNMethods",
"-XX:+PrintSignatureHandlers", "-XX:+PrintAdapterHandlers", "-XX:+PrintStubCode",
"-XX:+PrintCompilation", "-XX:+PrintInlining", "-XX:+TraceClassLoading",})
@Warmup(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS)
@Threads(4)
public class VolTest {
static class Obj1 {
int f1 = 0;
}
@State(Scope.Group)
public static class State1 {
volatile Obj1 v1 = new Obj1();
Obj1 v2 = new Obj1();
}
@Group @Benchmark @CompilerControl(CompilerControl.Mode.DONT_INLINE)
public void runVolT1(State1 s) {
Obj1 o = new Obj1(); /* 43 */
o.f1 = 1; /* 44 */
s.v1 = o; /* 45 */
}
@Group @Benchmark @CompilerControl(CompilerControl.Mode.DONT_INLINE)
public void runVolT2(State1 s) {
s.v2 = s.v1; /* 52 */
}
@Group @Benchmark @CompilerControl(CompilerControl.Mode.DONT_INLINE)
public int runVolT3(State1 s) {
return s.v1.f1; /* 59 */
}
@Group @Benchmark @CompilerControl(CompilerControl.Mode.DONT_INLINE)
public int runVolT4(State1 s) {
return s.v2.f1; /* 66 */
}
}
Вот сборка, сгенерированная JIT для runVolT3
и runVolT4
:
Compiled method (c1) 26806 529 2 org.sample.VolTest::runVolT3 (8 bytes)
...
[Constants]
# {method} {0x0000fff77cbc4f10} 'runVolT3' '(Lorg/sample/VolTest$State1;)I' in 'org/sample/VolTest'
# this: c_rarg1:c_rarg1
= 'org/sample/VolTest'
# parm0: c_rarg2:c_rarg2
= 'org/sample/VolTest$State1'
...
[Verified Entry Point]
...
;*aload_1 {reexecute=0 rethrow=0 return_oop=0}
; - org.sample.VolTest::runVolT3@0 (line 59)
0x0000fff781a60938: dmb ish
0x0000fff781a6093c: ldr w0, [x2, #12] ; implicit exception: dispatches to 0x0000fff781a60984
0x0000fff781a60940: dmb ishld ;*getfield v1 {reexecute=0 rethrow=0 return_oop=0}
; - org.sample.VolTest::runVolT3@1 (line 59)
0x0000fff781a60944: ldr w0, [x0, #12] ;*getfield f1 {reexecute=0 rethrow=0 return_oop=0}
; - org.sample.VolTest::runVolT3@4 (line 59)
; implicit exception: dispatches to 0x0000fff781a60990
0x0000fff781a60948: ldp x29, x30, [sp, #48]
0x0000fff781a6094c: add sp, sp, #0x40
0x0000fff781a60950: ldr x8, [x28, #264]
0x0000fff781a60954: ldr wzr, [x8] ; {poll_return}
0x0000fff781a60958: ret
...
Compiled method (c2) 27005 536 4 org.sample.VolTest::runVolT3 (8 bytes)
...
[Constants]
# {method} {0x0000fff77cbc4f10} 'runVolT3' '(Lorg/sample/VolTest$State1;)I' in 'org/sample/VolTest'
# this: c_rarg1:c_rarg1
= 'org/sample/VolTest'
# parm0: c_rarg2:c_rarg2
= 'org/sample/VolTest$State1'
...
[Verified Entry Point]
...
; - org.sample.VolTest::runVolT3@-1 (line 59)
0x0000fff788f692f4: cbz x2, 0x0000fff788f69318
0x0000fff788f692f8: add x10, x2, #0xc
0x0000fff788f692fc: ldar w11, [x10] ;*getfield v1 {reexecute=0 rethrow=0 return_oop=0}
; - org.sample.VolTest::runVolT3@1 (line 59)
0x0000fff788f69300: ldr w0, [x11, #12] ;*getfield f1 {reexecute=0 rethrow=0 return_oop=0}
; - org.sample.VolTest::runVolT3@4 (line 59)
; implicit exception: dispatches to 0x0000fff788f69320
0x0000fff788f69304: ldp x29, x30, [sp, #16]
0x0000fff788f69308: add sp, sp, #0x20
0x0000fff788f6930c: ldr x8, [x28, #264]
0x0000fff788f69310: ldr wzr, [x8] ; {poll_return}
0x0000fff788f69314: ret
...
Compiled method (c1) 26670 527 2 org.sample.VolTest::runVolT4 (8 bytes)
...
[Constants]
# {method} {0x0000fff77cbc4ff0} 'runVolT4' '(Lorg/sample/VolTest$State1;)I' in 'org/sample/VolTest'
# this: c_rarg1:c_rarg1
= 'org/sample/VolTest'
# parm0: c_rarg2:c_rarg2
= 'org/sample/VolTest$State1'
...
[Verified Entry Point]
...
;*aload_1 {reexecute=0 rethrow=0 return_oop=0}
; - org.sample.VolTest::runVolT4@0 (line 66)
0x0000fff781a604b8: ldr w0, [x2, #16] ;*getfield v2 {reexecute=0 rethrow=0 return_oop=0}
; - org.sample.VolTest::runVolT4@1 (line 66)
; implicit exception: dispatches to 0x0000fff781a604fc
0x0000fff781a604bc: ldr w0, [x0, #12] ;*getfield f1 {reexecute=0 rethrow=0 return_oop=0}
; - org.sample.VolTest::runVolT4@4 (line 66)
; implicit exception: dispatches to 0x0000fff781a60508
0x0000fff781a604c0: ldp x29, x30, [sp, #48]
0x0000fff781a604c4: add sp, sp, #0x40
0x0000fff781a604c8: ldr x8, [x28, #264]
0x0000fff781a604cc: ldr wzr, [x8] ; {poll_return}
0x0000fff781a604d0: ret
...
Compiled method (c2) 27497 535 4 org.sample.VolTest::runVolT4 (8 bytes)
...
[Constants]
# {method} {0x0000fff77cbc4ff0} 'runVolT4' '(Lorg/sample/VolTest$State1;)I' in 'org/sample/VolTest'
# this: c_rarg1:c_rarg1
= 'org/sample/VolTest'
# parm0: c_rarg2:c_rarg2
= 'org/sample/VolTest$State1'
...
[Verified Entry Point]
...
; - org.sample.VolTest::runVolT4@-1 (line 66)
0x0000fff788f69674: ldr w11, [x2, #16] ;*getfield v2 {reexecute=0 rethrow=0 return_oop=0}
; - org.sample.VolTest::runVolT4@1 (line 66)
; implicit exception: dispatches to 0x0000fff788f69690
0x0000fff788f69678: ldr w0, [x11, #12] ;*getfield f1 {reexecute=0 rethrow=0 return_oop=0}
; - org.sample.VolTest::runVolT4@4 (line 66)
; implicit exception: dispatches to 0x0000fff788f69698
0x0000fff788f6967c: ldp x29, x30, [sp, #16]
0x0000fff788f69680: add sp, sp, #0x20
0x0000fff788f69684: ldr x8, [x28, #264]
0x0000fff788f69688: ldr wzr, [x8] ; {poll_return}
0x0000fff788f6968c: ret
Отметим, что барьерные инструкции сгенерированная сборка содержит:
runVolT1
(the assembly isn't shown above because it's too long):
c1
version contains 1x dmb ishst
, 2x dmb ish
c2
версия содержит 1x dmb ishst
, 1x dmb ish
, 1x stlr
runVolT3
(which reads volatile v1
):
c1
version 1x dmb ish
, 1x dmb ishld
c2
версия 1x ldar
runVolT4
(что читается как энергонезависимая v2
): нет барьеров памяти
Как видите, runVolT4
(который читает объект после небезопасной перезаписи) не содержит барьеров памяти.
Означает ли это, что поток может видеть состояние объекта как полуинициализированное?
Оказывается, нет, тем не менее на ARMv8 это безопасно.
Почему?
Посмотрите на return s.v2.f1;
в коде. Здесь ЦП выполняет 2 чтения памяти:
- сначала он читает
s.v2
, который содержит адрес памяти объекта o
- затем он считывает значение
o.f1
из (адрес памяти o
) + (смещение поля f1
в пределах Obj1
)
Адрес памяти для чтения o.f1
вычисляется из значения, возвращаемого чтением s.v2
— это так называемая адресная зависимость.
В ARMv8 такая зависимость от адреса предотвращает изменение порядка этих двух операций чтения (см. пример MP+dmb.sy+addr
в Оперативное моделирование архитектуры ARMv8: параллелизм и ISA, вы можете сами попробовать в Инструмент моделирования памяти ARM), поэтому мы гарантированно увидим v2
полностью инициализированным.
Инструкции барьера памяти в runVolT3
служат другой цели: они предотвращают изменение порядка чтения volatile s.v1
с другими действиями в потоке (в Java чтение volatile является одним из действий синхронизации, которые должны быть полностью упорядочены).
Более того, сегодня оказывается на все поддерживаемые благодаря архитектуре OpenJDK адресная зависимость предотвращает переупорядочение операций чтения (см. Зависимые загрузки могут быть переупорядочены в этой таблица в вики или Заказы зависимостей данных загружаются? в таблице в JSR -133 Пособие для разработчиков компиляторов).
В итоге сегодня на OpenJDK если объект изначально публикуется через volatile поле, то он скорее всего будет виден как полностью инициализированный даже после небезопасной републикации.
<сильный>2. Первоначальная публикация через синхронизированный блок: скорее всего небезопасно
Иная ситуация, когда первоначальная публикация осуществляется через синхронизированный блок:
class Obj1 {
int f1 = 0;
}
Obj1 v1;
Obj1 v2;
Thread 1 | Thread 2 | Thread 3
--------------------------------------------------------
synchronized { | |
var o = new Obj1(); | |
o.f1 = 1; | |
v1 = o; | |
} | |
| synchronized { |
| var r1 = v1; |
| } |
| v2 = r1; |
| | var r2 = v2.f1;
Is (r2 == 0) possible?
Здесь сгенерированная сборка для Thread 3
такая же, как и для runVolT4
выше: она не содержит инструкций барьера памяти. В результате Thread 3
может легко увидеть неупорядоченные записи от Thread 1
.
И вообще, небезопасная републикация в таких случаях скорее всего небезопасна сегодня на OpenJDK.
person
Community
schedule
22.02.2021