16 декабря 2011 г.

final-поля для безопасной публикации

В догонку к обсуждениям семантики final-полей. Обычный класс, с изменямыми полями, сложно безопасно опубликовать через data race (т.е. без синхронизации):
sharedRef = new Mutable();
Но можно опубликовать через "обертку" -- класс с final-ссылкой:
sharedRef = new ImmutableReference(new Mutable());
И у меня давно уже бродит идея -- почему бы не сделать сам класс для себя "публикующей оберткой"?
public class SelfPublisher {

    private int value;

    private final SelfPublisher safeThis;

    public SelfPublisher( final int value ) {
        this.value = value;
        this.safeThis = this;
    }

    public int getValue(){
        return safeThis.value;
    }
}
Насколько я вижу -- это позволит совершенно безопасно публиковать его через даже data race. Ценой -- увы -- дополнительных затрат памяти на избыточное поле. Ну и дополнительной операции присваивания в конструкторе.

P.S. Код -- уродлив, по моим критериям изящества. Это, скорее, трюк, чем пример для подражания :) И use case для него тоже довольно туманен. Но сама идея мне показалась любопытной.

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

  1. случайно не возможна ли ситуация, на какой-нибудь экзотической архитектуре, когда компилятор (jit прежде всего) изменит порядок с program order на processor order, и соот-но value будет опубликовано после final поля

    ОтветитьУдалить
  2. Такое возможно только если соответствующая JVM нарушает модель памяти. Потому что спецификация final-полей (в предыдущей статье я ее разбирал) гарантирует, что через final-ссылку видимы все записи, которые завершились до завершения конструктора (а не до записи самой final-ссылки). Другими словами, то, что присвоение safeThis идет последним -- несущественно. Можно хоть в начало перенести, ничего не изменится.

    ОтветитьУдалить
  3. Как-то утекание ссылки на this из конструктора очень стремно выглядит. Надо будет еще раз твою предыдущую статью покурить, а то так сразу что-то не очень верится даже...

    ОтветитьУдалить
  4. Утекание this -- это ведь не означает "любое использование this" :) Утекание -- это публикация ссылки на объект до завершения конструктора. Более того, опубликованная ссылка должна быть доступна другим потокам, кроме инициализировавшего. Публикация до заморозки -- вот что такое утекание.

    А здесь такого нет -- ссылка на объект никуда не уходит до завершения конструктора.

    ОтветитьУдалить
  5. В безопасной публикации через final поля меня всегда больше интересовала реальная область применения этой штуки. Ведь Ссылку на sharedRef которая указывает на обертку с final-ссылкой тоже надо как-то получить. Мне конечно понятна что если я вижу sharedRef являющуюся не null, то дальше все круто. Т.е. Мне получается надо всегда проверять эту sharedRef на null, а Если мне нужно иметь ее не null, то мне полюбому потребуются дополнительные средства синхронизации.

    ОтветитьУдалить
  6. Ну "проверять ссылку на не-null" тебе придется всегда -- даже с использованием синхронизации у тебя все равно будет конструкция вида synchronized(lock){ while(sharedRef!=null){ lock.wait() } }

    Семантика final-полей дает тебе возможность делать такие же циклы только без synchronized/wait. Просто один поток создает объект и публикует ссылку, второй сидит в spin-wait на sharedRef!=null.

    По-сути, это возможность корректно передавать данные через data-race. В JMM есть весьма важные базовые гарантии -- типа атомарности записи для большинства примитивных типов -- которые позволяют делать определенные выводы даже для кода с гонками (чего обычно стараются избегать). Но все эти выводы применимы только к каким-то очень несложным конструкциям на основе только примитивных типов. Стоит только попробовать доказать корректность какого-то более реалистичного кода с гонками, как количество возможных трасс взрывает мозг. final-поля позволяют делать большие объекты "примитивными" с точки зрения модели памяти -- т.е. распространять на них атомарность. Точно так же, как для строчки sharedInt = 42 JMM гарантирует, что видимыми являются либо состояние до присвоения, либо после -- но никакое промежуточное, точно так же для shareRef = new ImmutableObject() JMM гарантирует то же самое.

    ОтветитьУдалить
  7. "видимыми являются либо состояние до присвоения, либо после" - ну вобщем да, в этом и вся фича final если кратко получается.

    По воду же:
    "Семантика final-полей дает тебе возможность делать такие же циклы только без synchronized/wait. Просто один поток создает объект и публикует ссылку, второй сидит в spin-wait на sharedRef!=null."
    spin-wait - это же wait, который ты без захватра монитора не выполнишь. А там где захват монитора, там и happens-before ребра, т.е. никакие гарантии final уже не нужны. Наверное, ты имел ввиду spin-sleep в терминах java. Ну или остается active spinning, хотя я вообще это никогда не использовал.

    ОтветитьУдалить
  8. Не, я имел в виду busy-wait -- когда поток просто в цикле читает переменную, без какой-либо синхронизации. spin-wait я использую в смысле "ожидание за счет spinning". Иногда добавляется yield/sleep в качестве backoff.

    ОтветитьУдалить
  9. Руслан, сорри за надоедливость, но все же спрошу. Как часто ты используешь в повседнемном программировании final поля для безопасной публикации? Вот я, хоть убей не могу вспомнить, что написал final в каком-то месте именно из этих соображений и понимал, что без него может и не сработать. Хотя с многопоточностью приходилось работать частенько засучив рукава в нескольких разных проектах. Может конечно я делал это на автомате, не вдумываясь, используя привычные конструкции. Во всем что я припоминаю, всегда использовались ребра happens-before либо в виде передачи таска через blocking queue, либо через start() метод треда.

    ОтветитьУдалить
  10. Ты меня поймал :) Именно для безопасной публикации через data race -- очень редко. Безблокировочный кэш, который я описывал летом -- единственное решение, которое реально работает в текущем проекте. И то -- больше потому, что мне очень хотелось его потестить в продакшене. Реально можно было бы реализовать подобное и без эзотерических финтов, потеряв около 15-25% производительности, что вряд ли было бы критично.

    Зато я очень часто использую неизменяемые объекты как "гарантированно потокобезопасные объекты". Вот здесь без final тяжело -- на позапрошлой (?) неделе в concurrency-interest был большой тред на тему "как мне сделать свой объект пуленепробиваемо потоковобезопасным" (т.е. безопасным независимо от наличия гонок в его использовании) . У автора некий объект (часть public api) сначала был просто immutable, а потом ему пришлось добавить не-final-поле. И ему хотелось с помощью synchronized внутри объекта обеспечить не меньшие гарантии, чем дает final.

    ОтветитьУдалить
  11. > spin-wait я использую в смысле "ожидание за счет spinning". Иногда добавляется yield/sleep в качестве backoff.

    http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.3
    Вроде как не sleep не yield нельзя использовать при ожидании условия, так как они не имеют семантики синхронизации:

    For example, in the following (broken) code fragment, assume that this.done is a non-volatile boolean field:

    while (!this.done)
    Thread.sleep(1000);

    The compiler is free to read the field this.done just once, and reuse the cached value in each execution of the loop. This would mean that the loop would never terminate, even if another thread changed the value of this.done.

    ОтветитьУдалить
    Ответы
    1. Семантика синхронизации здесь появляется как раз за счет final. Но гарантии прогресса здесь действительно не будет, компилятор свободен закэшировать переменную в цикле хоть навсегда. Проблема в том, что и с синхронизацией гарантий прогресса тоже, на самом-то деле, нет.

      Удалить