Когда во время строительства объект java становится ненулевым?
Предположим, что вы создаете объект java, например:
SomeClass someObject = null;
someObject = new SomeClass();
В какой момент someObject становится непустым? До запуска конструктора SomeClass()
или после?
Чтобы немного разъяснить, скажем, если другой поток должен был проверить, был ли someObject
равным null, а конструктор SomeClass()
находился на полпути к завершению, было бы оно нулевым или ненулевым?
Кроме того, какая разница, если someObject
было создано так:
SomeClass someObject = new SomeClass();
Может ли someObject
быть null?
Ответы
Ответ 1
Если другой поток должен был проверить переменную someObject
"во время", я полагаю, что она может (из-за особенностей в модели памяти) увидеть частично инициализированный объект. Новая (как и для Java 5) модель памяти означает, что любые конечные поля должны быть установлены до их значений до того, как объект станет видимым для других потоков (пока ссылка на вновь созданный объект не выйдет из конструктора в любом другом путь), но помимо этого существует не так много гарантий.
В принципе, не обменивайтесь данными без соответствующей блокировки (или гарантии, предоставляемые статическими инициализаторами и т.д.):) Серьезно, модели памяти серьезно сложны, как и программирование в режиме блокировки вообще. Постарайтесь, чтобы это не стало возможным.
В логических условиях присваивание происходит после запуска конструктора, поэтому, если вы наблюдаете переменную из того же потока, она будет равна нулю во время вызова конструктора. Однако, как я уже сказал, есть странности модели памяти.
EDIT: для целей двойной проверки блокировки вы можете избежать этого, если ваше поле volatile
и если вы используете Java 5 или выше. До появления Java 5 модель памяти для этого не была достаточно сильной. Тем не менее, вам нужно получить шаблон. Подробнее см. "Эффективная Java", 2-е издание, п. 71.
EDIT: Здесь мои рассуждения о том, чтобы спорить с аароновой вставкой, видимой в одном потоке. Предположим, что:
public class FooHolder
{
public static Foo f = null;
public static void main(String[] args)
{
f = new Foo();
System.out.println(f.fWasNull);
}
}
// Make this nested if you like, I don't believe it affects the reasoning
public class Foo
{
public boolean fWasNull;
public Foo()
{
fWasNull = FooHolder.f == null;
}
}
Я считаю, что это всегда будет сообщать true
. Из раздел 15.26.1:
В противном случае требуются три шага:
- Сначала левый операнд оценивается для создания переменной. Если эта оценка завершается внезапно, тогда назначение выражение резко завершается для та же самая причина; правый операнд не оценивается и не назначается происходит.
- В противном случае оценивается правый операнд. Если это оценка завершается резко, затем выражение присваивания завершается внезапно по той же причине и нет происходит присвоение.
В противном случае значение правого операнд преобразуется в тип левая переменная, подвергается к преобразованию значений (§5.1.13) в соответствующее стандартное значение (а не набор значений расширенного экспонента), и результат преобразования хранится в переменной.
Затем из раздел 17.4.5:
Два действия могут быть упорядочены с помощью отношения "происходить-до". Если одно действие происходит - перед другим, то первое видно и упорядочивается до второго.
Если у нас есть два действия x и y, мы пишем hb (x, y), чтобы указать, что x происходит до y.
- Если x и y - действия одного и того же потока, а x - до y в программном порядке, то hb (x, y).
- Для этого объекта существует конец до конца конструктора объекта до начала финализатора (§12.6).
- Если действие x синхронизируется со следующим действием y, то мы также имеем hb (x, y).
- Если hb (x, y) и hb (y, z), то hb (x, z).
Следует отметить, что наличие взаимосвязи между двумя действия не обязательно означают, что они должны иметь место в этом порядке в реализация. Если переупорядочение приводит к результатам, соответствующим законному исполнению, это не является незаконным.
Другими словами, это нормально для странных вещей, которые происходят даже в пределах одного потока, но это не должно быть наблюдаемым. В этом случае разница будет наблюдаемой, поэтому я считаю, что это было бы незаконно.
Ответ 2
someObject
станет не-t21 в какой-то момент конструкции. Как правило, есть два случая:
- Оптимизатор ввел конструктор
- Конструктор не встроен.
В первом случае VM выполнит этот код (псевдокод):
someObject = malloc(SomeClass.size);
someObject.field = ...
....
Итак, в этом случае someObject
не null
, и он указывает на память, которая не инициализируется на 100%, а именно не весь код конструктора запущен! Вот почему дважды проверенная блокировка не работает.
Во втором случае код из конструктора будет запущен, ссылка будет передана обратно (точно так же, как в обычном вызове метода), а someObject будет установлен на значение ссылки после того, как все и каждый код инициализации будет запущен.
Проблема заключается в том, что нет возможности сказать java не назначать someObject
раньше. Например, вы можете попробовать:
SomeClass tmp = new SomeClass();
someObject = tmp;
Но поскольку tmp не используется, оптимизатору разрешено его игнорировать, поэтому он будет генерировать тот же код, что и выше.
Таким образом, это позволяет оптимизатору создавать более быстрый код, но при написании многопоточного кода он может укусить вас. В однопоточном коде это обычно не проблема, поскольку код не выполняется до завершения конструктора.
[EDIT] Вот хорошая статья, которая объясняет, что происходит: http://www.ibm.com/developerworks/java/library/j-dcl.html
PS: Книга " Эффективная Java, вторая редакция" Джошуа Блоха содержит решение для Java 5 и выше:
private volatile SomeClass field;
public SomeClass getField () {
SomeClass result = field;
if (result == null) { // First check, no locking
synchronized(this) {
result = field;
if (result == null) { // second check with locking
field = result = new SomeClass ();
}
}
}
return result;
}
Выглядит странно, но должен работать на каждой виртуальной машине Java. Обратите внимание, что каждый бит важен; если вы опустите двойное назначение, вы либо получите плохую производительность, либо частично инициализировали объекты. Чтобы получить полное объяснение, купите книгу.
Ответ 3
someObject
будет нулевым указателем до тех пор, пока ему не будет присвоено значение указателя от конструктора типа. Поскольку назначение справа налево, возможно для другого потока, чтобы проверить someObject
, пока конструктор все еще работает. Это было бы до назначения указателя на переменную, поэтому someObject
все равно будет иметь значение null.
Ответ 4
Из другого потока ваш объект по-прежнему будет выглядеть нулевым, пока конструктор не завершит выполнение. Вот почему, если построение завершается исключением, ссылка останется пустой.
Object o = null;
try {
o = new CtorTest();
} catch (Exception e) {
assert(o == null); // i will be null
}
где
class CtorTest {
public CtorTest() {
throw new RuntimeException("Ctor exception.");
}
}
Убедитесь, что вы синхронизируете другой объект, а не тот, который был сконструирован.
Ответ 5
Вот пример тестового кода, который показывает, что объект имеет значение null, пока конструктор не закончит работу:
public class Test {
private static SlowlyConstructed slowlyConstructed = null;
public static void main(String[] args) {
Thread constructor = new Thread() {
public void run() {
Test.slowlyConstructed = new SlowlyConstructed();
}
};
Thread checker = new Thread() {
public void run() {
for(int i = 0; i < 10; i++) {
System.out.println(Test.slowlyConstructed);
try { Thread.sleep(1000); }
catch(Exception e) {}
}
}
};
checker.start();
constructor.start();
}
private static class SlowlyConstructed {
public String s1 = "s1 is unset";
public String s2 = "s2 is unset";
public SlowlyConstructed() {
System.out.println("Slow constructor has started");
s1 = "s1 is set";
try { Thread.sleep(5000); }
catch (Exception e) {}
s2 = "s2 is set";
System.out.println("Slow constructor has finished");
}
public String toString() {
return s1 + ", " + s2;
}
}
}
Вывод:
null
Slow constructor has started
null
null
null
null
null
Slow constructor has finished
s1 is set, s2 is set
s1 is set, s2 is set
s1 is set, s2 is set
s1 is set, s2 is set
Ответ 6
Для вашего первого примера: someObject становится ненулевым ПОСЛЕ завершения конструктора. Если вы проверите из другого потока, someObject станет ненулевым после завершения конструктора. Опасайтесь, вы никогда не должны обращаться к несинхронизированным объектам из разных потоков, поэтому ваш пример не должен реализовываться таким образом в реальном коде.
Для второго примера someObject никогда не будет пустым, поскольку он будет построен ПОСЛЕ ТОГО, КАК будет построен SomeClass, и создается некоторый объект и инициализируется вновь созданным объектом. То же самое и для потоков: не обращайтесь к этой переменной из разных потоков без синхронизации!