Лучшее понимание того, почему этот тест не проходит, может быть получено из понимания того, что на самом деле происходит при вызове конструктора.Java - это основанный на стеке язык.TestClass.f = new TestClass();
состоит из четырех действий.Сначала вызывается new
инструкция, она похожа на malloc в C / C ++, она выделяет память и помещает ссылку на нее сверху стека.Затем ссылка дублируется для вызова конструктора.Конструктор фактически похож на любой другой метод экземпляра, он вызывается с дублированной ссылкой.Только после этого ссылка сохраняется во фрейме метода или в поле экземпляра и становится доступной откуда угодно.Перед последним шагом ссылка на объект присутствует только в верхней части стека потока, и никто другой не сможет его увидеть.На самом деле нет разницы, с каким типом поля вы работаете, оба будут инициализированы, если TestClass.f != null
.Вы можете читать поля x и y из разных объектов, но это не приведет к y = 0
.Для получения дополнительной информации вы должны увидеть Спецификация JVM и Язык стекового программирования статьи.
UPD : одну важную вещь, которую я забыл упомянуть.По Java-памяти нет возможности увидеть частично инициализированный объект.Если вы не делаете самостоятельные публикации внутри конструктора, обязательно.
JLS :
Объект считается полностью инициализированным после завершения его конструктора.Поток, который может видеть ссылку на объект только после того, как этот объект был полностью инициализирован, гарантированно увидит корректно инициализированные значения для конечных полей этого объекта.
JLS :
Существует ребро перед событием от конца конструктора объекта до начала финализатора для этого объекта.
Более широкое объяснение этоготочка зрения :
Оказывается, что конец конструктора объекта происходит до выполнения его метода finalize.На практике это означает, что любые записи, которые происходят в конструкторе, должны быть завершены и видимы для любых операций чтения той же переменной в финализаторе, как если бы эти переменные были изменчивыми.
UPD : Это была теория, давайте перейдем к практике.
Рассмотрим следующий код с простыми неконечными переменными:
public class Test {
int myVariable1;
int myVariable2;
Test() {
myVariable1 = 32;
myVariable2 = 64;
}
public static void main(String args[]) throws Exception {
Test t = new Test();
System.out.println(t.myVariable1 + t.myVariable2);
}
}
Следующая команда отображает сгенерированные машинные инструкциикак использовать его, вы можете найти в вики :
java.exe -XX: + UnlockDiagnosticVMOptions -XX: + PrintAssembly -Xcomp -XX: PrintAssemblyOptions = hsdis-print-bytes -XX: CompileCommand = print, * Test.main Test
Вывод:
...
0x0263885d: movl $0x20,0x8(%eax) ;...c7400820 000000
;*putfield myVariable1
; - Test::<init>@7 (line 12)
; - Test::main@4 (line 17)
0x02638864: movl $0x40,0xc(%eax) ;...c7400c40 000000
;*putfield myVariable2
; - Test::<init>@13 (line 13)
; - Test::main@4 (line 17)
0x0263886b: nopl 0x0(%eax,%eax,1) ;...0f1f4400 00
...
За полевыми присваиваниями следует инструкция NOPL одной из его целей является предотвращение переупорядочения команд .
Почему это происходит? Согласно спецификации финализация происходит после возврата конструктора.Таким образом, поток GC не может видеть частично инициализированный объект.На уровне ЦП поток GC не отличается от любого другого потока.Если такие гарантии предоставляются GC, то они предоставляются любому другому потоку.Это наиболее очевидное решение для такого ограничения.
Результаты:
1) Конструктор не синхронизирован, синхронизация выполняется другими инструкциями .
2) Присвоение ссылки на объект не может быть выполнено до возврата конструктора.