6 мая 2012 г.

Вдогонку про публикацию

Вдогонку к предыдущему посту, и к вот этому посту Алексея. Для прояснения ситуации.

Итак, вот это
public class T {
   public int field1;
 
   public T( final int field1 ){
      this.field1 = field1;
   }
}
 
//...где-то в дебрях тундры...
public T sharedRef;
 
sharedRef = new T( 10 );
 
НЕбезопасная публикация

Вот это:
public class T {
   public int field1;
 
   public T( final int field1 ){
      this.field1 = field1;
   }
}
 
//...где-то в дебрях кода...
public volatile T sharedRef;
 
sharedRef = new T( 10 );
 
безопасная публикация -- потому что через volatile-ссылку.

Вот это:
public class T {
   public final int field1;
 
   public T( final int field1 ){
      this.field1 = field1;
   }
}
 
//...где-то в дебрях кода...
public T sharedRef;
 
sharedRef = new T( 10 );
 
безопасная публикация -- потому что для final-полей особые гарантии.

А вот это:
public class T {
   public volatile int field1;
 
   public T( final int field1 ){
      this.field1 = field1;
   }
}
 
//...где-то в дебрях кода...
public T sharedRef;
 
sharedRef = new T( 10 );
 

НЕбезопасная публикация -- потому что обычные записи (sharedRef=new T()) можно переставлять до волатильных записей! Поэтому волатильность поля field1 не гарантирует нам, что ссылка на объект T будет опубликована после инициализации этого поля.

UPD: В данном конкретном случае, когда в классе всего одно поле — публикация таки безопасна. Дискуссию по этому поводу можно прочитать здесь, а формальное доказательство здесь. Однако неочевидно, можно ли перенести это рассуждение на классы со "смешанным составом" полей (volatile+non volatile)

UPD по просьбам читателей: вот это
public class T {
   private int field1;
 
   public T( final int field1 ){
      synchronized( this ) {
         this.field1 = field1;
      }
   }
    
   public synchronized int getField1(){
      return this.field1;
   }
   
   public synchronized void setField1( final int field1 ){
      this.field1 = field1;
   }
}
 
//...где-то в дебрях кода...
public T sharedRef;
 
sharedRef = new T( 10 );
 
Это такая хитрая штука: формально публикация небезопасна, но фактически это ненаблюдаемо -- если вы не полезете к field1 какими-нибудь неправославными методами, то для доступа через геттер/сеттер все безопасно.

10 комментариев:

  1. Выдайте Руслану инвайт уже, кто-нибудь :)

    ОтветитьУдалить
  2. чтобы я ушел из своего уютного бложика???

    ОтветитьУдалить
  3. Этот комментарий был удален автором.

    ОтветитьУдалить
  4. Нет, чтобы ты мог троллить в комментах :)

    ОтветитьУдалить
  5. О, на такое я согласен. Дайте, дайте же мне инвайт скорее!!!

    ОтветитьУдалить
  6. Если мы публикуем объект immutable класса не через volatile поле, то ведь нет гарантии, что второй thread вообще когда нибудь увидит, что эта ссылка проинициализована? Вроде с точки зрения JMM только есть гарантия, что если второй поток увидит не null ссылку, то это будет корректно проинициализированный класс.
    В случае публикации с использованием volatile ссылки второй поток увидит проиницилизированную ссылку если читает хронологически после того, как первый поток туда записал инстанс класса. Кроме того, как Вы отмечаете, класс так же будет корректно инстанциирован.
    Вопрос, а не следует ли использовать синхронизацию при записи ссылки на объект immutable класса, так как мы наверное хотим, чтобы второй тред увидел записанное значение как можно скорее? А в таком случае зачем нужны inimitable классы, если все равно использовать volatile?

    ОтветитьУдалить
  7. Да, гарантии что запись вообще когда-то станет видимой -- нет. Но вы ошибаетесь, полагая, что такая гарантия есть в случае с volatile. Никакого отношения "хронологически после" в JMM нет, про часы JMM вообще ничего не говорит. happens before/after не имеют отношения к физическому времени. И синхронизация тоже не обеспечивает вам гарантии прогресса.

    Вообще, JMM это инструмент доказательства корректности, но не инструмент доказательства прогресса (хотя в каких-то отдельных случаях гарантии прогресса вроде бы можно вывести из JMM -- но в общем случае нельзя).

    Но кроме JMM есть еще практические соображения. И из практических соображений мы знаем, что JIT-ы реализуют volatile через довольно жесткий барьер памяти (хотя JMM и не обязывает делать именно так). И мы знаем, что этот барьер реализуется разработчиками процессоров через store buffers flush или что-то очень близкое к нему (хотя, опять же, они и не обязаны реализовывать его именно так -- это лишь самый простой вариант). И поэтому таки-да, в реальной жизни делая volatile store вы можете быть почти уверены, что следующее же в хронологическом порядке (задаваемом тиками шины, а вовсе не System.nanoTime(), скажем) volatile read его скорее всего прочитает. Делая же обычный store вы имеете меньше оснований для такой уверенности -- потому что и компилятор может задержать запись, оставив пока значение в регистре, и уже выполненная запись может задержаться в store buffer-е процессора.

    Но, хотя JMM теоретически позволяет в некоторых случаях откладывать запись хоть навсегда, на практике, опять же, это крайне редко встречается. Если ваше приложение хоть что-то делает дальше -- рано или поздно буфер записи переполнится, рано или поздно компилятор будет вынужден сбросить значение из регистра в память. Подобрать реальную ситуацию где запись без volatile застрянет в одном потоке навечно крайне сложно. Но таки да, задержка может быть на несколько десятков тактов больше, чем с volatile

    ОтветитьУдалить
  8. Этот комментарий был удален автором.

    ОтветитьУдалить
  9. Почему вы утверждаете , что если volatile store и volatile read идут в хронологическом порядке, то если вероятность, пусть и небольшая, что volatile read ничего не увидит? Разве это не нарушает sequential consistency? Каким в таком случае образом, JMM определяет volatile?

    ОтветитьУдалить
  10. >Почему вы утверждаете , что если volatile store и volatile read идут в хронологическом порядке, то если вероятность, пусть и небольшая, что volatile read ничего не увидит

    Я могу это утверждать, потому что скорость света конечна, и вы не можете увидеть результат события, случившегося на одном ядре, наблюдая с другого ядра, мгновенно. А раз так, то я всегда могу задать интервал времени, в течении которого vread на втором ядре будет видеть "старое" значение ячейки памяти.

    Более общо, потому что непонятно, что такое "хронологический порядок". Вопрос здесь сводится, если фундаментально, к тому, как именно вы синхронизируете часы между различными ядрами. И этот вопрос совсем не тривиален -- он ровно настолько не тривиален, как и соответствующий вопрос в специальной теории относительности, и оригинальная статья Лампорта 70-х годов -- где впервые была введена нотация happens-before -- она явно отсылает к СТО в качестве источника вдохновения. Собственно, и результаты там очень похожие получаются. А именно -- не порядок событий (happens before) определяется каким-то абстрактным хронологическим порядком, а сам хронологический порядок задается расширяя happens-before.

    Если возвращаться к JMM, то System.currentTimeMillis/nanoTime не обязаны быть согласованы с порядком happens before. nanoTime() вообще не обязан возвращать одинаковое значения на разных ядрах.

    >Каким в таком случае образом, JMM определяет volatile?

    В самом деле -- как? ;)

    ОтветитьУдалить