Какова видимость в памяти переменных, доступ к которым осуществляется в статических синглтонах в Java?

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

public class GlobalData {

    // Data-related code. This could be anything; I've used a simple String.
    //
    private String someData;
    public String getData() { return someData; }
    public void setData(String data) { someData = data; }

    // Singleton code
    //
    private static GlobalData INSTANCE;
    private GlobalData() {}
    public synchronized GlobalData getInstance() { 
       if (INSTANCE == null) INSTANCE = new GlobalData();
       return INSTANCE; 
    }
}

Я надеюсь, что легко увидеть, что происходит. Можно вызвать GlobalData.getInstance().getData() в любое время в любом потоке. Если два потока вызывают setData() с разными значениями, даже если вы не можете гарантировать, какой из них «победит», меня это не беспокоит.

Но потокобезопасность здесь меня не волнует. Что меня беспокоит, так это видимость памяти. Всякий раз, когда в Java возникает барьер памяти, кэшированная память синхронизируется между соответствующими потоками. Барьер памяти возникает при прохождении синхронизаций, доступе к volatile-переменным и т.д.

Представьте себе следующий сценарий, происходящий в хронологическом порядке:

// Thread 1
GlobalData d = GlobalData.getInstance();
d.setData("one");

// Thread 2
GlobalData d = GlobalData.getInstance();
d.setData("two");

// Thread 1
String value = d.getData();

Возможно ли, что последнее значение value в потоке 1 все еще может быть "one"? Причина в том, что поток 2 никогда не вызывал никаких синхронизированных методов после вызова d.setData("two"), поэтому никогда не было барьера памяти? Обратите внимание, что барьер памяти в этом случае возникает каждый раз, когда вызывается getInstance(), потому что он синхронизирован.


person Matt Quigley    schedule 12.08.2013    source источник
comment
Вы говорите One can call GlobalData.getInstance().getData() at any time on any thread. It's thread-safe so I'm not worried about that, но на самом деле это не потокобезопасно именно из-за сценария, который вы описываете. Потокобезопасность заключается не только в том, чтобы видеть объект в несогласованном состоянии. Это также касается просмотра устаревших данных.   -  person yair    schedule 13.08.2013
comment
@yair Да, но я действительно пытался изучить аспект видимости памяти, а не несогласованные состояния, основанные на синхронном выполнении.   -  person Matt Quigley    schedule 13.08.2013


Ответы (3)


Возможно ли, что последнее значение value в потоке 1 все еще может быть «единицей»?

Да, это так. Модель памяти Java основана на отношениях «происходит до» (hb). В вашем случае у вас есть только getInstance выход — перед последующим getInstance входом из-за синхронизированного ключевого слова.

Итак, если мы возьмем ваш пример (при условии, что чередование потоков происходит в таком порядке):

// Thread 1
GlobalData d = GlobalData.getInstance(); //S1
d.setData("one");

// Thread 2
GlobalData d = GlobalData.getInstance(); //S2
d.setData("two");

// Thread 1
String value = d.getData();

У вас S1 хб S2. Если бы вы вызвали d.getData() из Thread2 после S2, вы бы увидели «один». Но не гарантируется, что последнее чтение d увидит «два».

person assylias    schedule 12.08.2013

Вы абсолютно правы.

Нет гарантии, что записи в одном Thread будут видны - это другой.

Чтобы обеспечить эту гарантию, вам нужно будет использовать ключевое слово volatile:

private volatile String someData;

Кстати, вы можете использовать загрузчик классов Java для обеспечения потокобезопасной ленивой инициализации вашего синглтона, как описано здесь . Это позволяет избежать ключевого слова synchronized и, следовательно, избавляет вас от некоторой блокировки.

Стоит отметить, что в настоящее время принятой передовой практикой является использование enum для хранения одноэлементных данных в Java.

person Boris the Spider    schedule 12.08.2013

Правильно, возможно, что поток 1 по-прежнему видит значение как «единицу», так как не произошло событие синхронизации памяти и не произошло ни одной связи между потоком 1 и потоком 2 (см. раздел 17.4.5 JLS).

Если бы someData было volatile, тогда поток 1 увидит значение как «два» (при условии, что поток 2 завершился до того, как поток 1 получил значение).

Наконец, не по теме, реализация синглтона немного далека от идеала, поскольку он синхронизируется при каждом доступе. Как правило, лучше использовать перечисление для реализации синглтона или, по крайней мере, назначить экземпляр в статическом инициализаторе, чтобы в методе getInstance не требовался вызов конструктора.

person Trevor Freeman    schedule 12.08.2013