Безопасен ли доступ по ссылке на лениво инициализированный энергонезависимый поток String? - PullRequest
0 голосов
/ 11 апреля 2019

У меня есть поле String, которое инициализируется на null, но затем может быть доступно более чем одному потоку.Значение будет лениво инициализировано до идемпотентно рассчитанного значения при первом доступе.

Требуется ли для этого поля значение volatile, чтобы обеспечить безопасность потока?

Вот пример.

public class Foo {
    private final String source;
    private String BAR = null;

    public Foo(String source) {
        this.source = source;
    }

    private final String getBar() {
        String bar = this.BAR;
        if (bar == null) {
            bar = calculateHashDigest(source); // e.g. an sha256 hash
            this.BAR = bar;
        }
        return bar;
    }

    public static void main(String[] args) {
        Foo foo = new Foo("Hello World!");
        new Thread(() -> System.out.println(foo.getBar())).start();
        new Thread(() -> System.out.println(foo.getBar())).start();
    }
}

Я использовал System.out.println() для примера, но меня не волнует, что происходит, когда его вызовы блокируются.(Хотя я почти уверен, что это также потокобезопасно.)

Нужно ли BAR быть volatile?

Я думаю, что ответ Нет , volatile не требуется, и Да это потокобезопасно, в основном из-за этой выдержки из JLS 17.5 :

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

И так как char value[] поле String действительно final.

(int hash не final, но его ленивая инициализация выглядит также хорошо.)

Редактировать : Изменить, чтобы уточнить значение, предназначенное для BAR, является фиксированным значением.Его расчет идемпотентен и не имеет побочных эффектов.Я не против, если вычисление повторяется между потоками, или BAR становится эффективно локальным из-за кэширования памяти / видимости.Меня беспокоит то, что если оно не равно нулю, то его значение является полным, а не каким-то частичным.

Ответы [ 2 ]

2 голосов
/ 11 апреля 2019

Ваш код (технически) не является поточно-ориентированным.

Это правда, что String - это правильно реализованный неизменяемый тип, и то, что вы говорите о его полях final, является правильным.Но это не та проблема безопасности потоков.

Первая проблема заключается в том, что при ленивой инициализации BAR возникает состояние гонки.Если два потока вызывают getBar() одновременно, они оба увидят BAR как null, и оба затем попытаются инициализировать его.

Вторая проблема заключается в опасности памяти.Поскольку между записью одного потока в BAR и последующим чтением другого потока в BAR не существует отношений случай-до , нет гарантии, что второй поток увидит инициализированное значение BAR.Следовательно, он может повторить инициализацию.

Обратите внимание, что в примере , как написано , эти две проблемы не являются практической проблемой безопасности потоков.Вы выполняете инициализацию идемпотент .Не имеет значения поведение кода, который вы можете инициализировать BAR несколько раз, поскольку вы всегда инициализируете его ссылкой на один и тот же объект String.(Стоимость одной избыточной инициализации слишком мала, чтобы о ней беспокоиться.)

Однако, если BAR была ссылкой на изменяемый объект или если инициализация была дорогой, то это реальная безопасность потоковпроблема.

Как говорит @Ravindra, простое решение - объявить getBar равным synchronized.Это решает обе проблемы.

Ваша идея объявить BAR обращается к опасности памяти, но не к состоянию гонки.


Вы добавили в свой Вопрос следующее:

Редактировать , чтобы уточнить значение, предназначенное для BAR, является фиксированным значением.Его расчет идемпотентен и не имеет побочных эффектов.Я не против, если вычисление повторяется между потоками, или BAR становится эффективно локальным из-за кэширования / видимости памяти.Меня беспокоит то, что если оно ненулевое, тогда его значение является полным, а не каким-то частичным.

Это ничего не меняет, как я сказал выше.Если значение равно String, то это правильно реализованный неизменяемый объект, и вы всегда увидите полное значение независимо от чего-либо еще.Это то, что говорится в цитате JLS!

(На самом деле, я скрываю детали, которые String использует не-1058 * поле для хранения лениво вычисленного хеш-кода. Однако String::hashCodeРеализация позаботится об этом. С этим нет проблем с безопасностью потоков. Проверьте сами, если хотите.)

1 голос
/ 11 апреля 2019

Ваш код не является потокобезопасным.Похоже, вы думаете о двойной проверке схемы блокировки.Правильный шаблон выглядит примерно так:

public class Foo {

    private static volatile String BAR = null;

    private static String getBar() {
        String bar = BAR;
        if (bar == null) {
          synchronized( Foo.class )
            if( bar == null ) {
              bar = "Hello World!";
              BAR = bar;
            }
        }
        return bar;
    }
    // ...

Итак, две вещи здесь.

  1. Если BAR уже инициализирован, блок synchronized непоступил.volatile здесь необходимо, потому что необходима некоторая синхронизация, и чтение BAR будет синхронизировано с записью в энергозависимый BAR.

  2. Если BAR равно нулю, томы входим в блок synchronized, мы должны еще раз проверить, что BAR по-прежнему равен нулю, чтобы мы могли выполнить проверку и присвоение атомарно.Если мы не проверяем атомарно, есть вероятность, что BAR будет инициализирован более одного раза.

Вы цитировали спецификацию Java.о ключевом слове final.Хотя String является неизменным и использует ключевое слово final внутри, это не влияет на ваше поле BAR.Строка в порядке, но ваше поле все еще находится в общей памяти, и его нужно синхронизировать, если вы ожидаете, что оно будет поточно-ориентированным.

Также еще один участник упомянул интернирующие строки.Правильно сказать, что в этом конкретном случае будет только один объект "Hello World!", потому что спецификация JVM гарантирует, что строки интернированы.Это странная форма безопасности потоков, которая не работает для других объектов, поэтому используйте ее только тогда, когда вы уверены, что она будет работать правильно.Большинство созданных вами объектов не смогут использовать код, который есть у вас, как сейчас.

Наконец, я подумал, что должен указать, что, поскольку "Hello World!" уже является строковым объектом, не так уж многоТочка в попытке "ленивой нагрузки" это.Строки создаются JVM при загрузке вашего класса, поэтому они уже существуют к моменту запуска вашего метода или даже к моменту чтения BAR в первый раз.В этом случае при использовании только строки нет смысла пытаться «лениво загрузить» строку.

public class Foo {

    //  probably better, simpler
    private static final String BAR = "Hello World!";
...