Являются ли конечные статические переменные потокобезопасными в Java?

Я довольно много читал, но не нашел окончательного ответа.

У меня есть класс, который выглядит так:

    public class Foo() {

        private static final HashMap<String, HashMap> sharedData;

        private final HashMap myRefOfInnerHashMap;

        static {
           // time-consuming initialization of sharedData
           final HashMap<String, String> innerMap = new HashMap<String, String>;
           innerMap.put...
           innerMap.put...
           ...a

           sharedData.put(someKey, java.util.Collections.unmodifiableMap(innerMap));
        }

        public Foo(String key) {
            this.myRefOfInnerHashMap = sharedData.get(key);
        }

        public void doSomethingUseful() {
            // iterate over copy
            for (Map.Entry<String, String> entry : this.myRefOfInnerHashMap.entrySet()) {
                ...
            }
        }
     }

И мне интересно, безопасен ли потокобезопасный доступ к sharedData из экземпляров Foo (как показано в конструкторе и в doSomethingUseful ()). Многие экземпляры Foo будут созданы в многопоточной среде.

Мое намерение состоит в том, чтобы sharedData инициализировалась в статическом инициализаторе и после этого не изменялась (только для чтения).

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

Другой конструкцией, которую я нашел, была ConcurrentHashMap. Я мог бы сделать sharedData типа ConcurrentHashMap, но должны ли содержащиеся в нем HashMaps быть типа ConcurrentHashMap? В основном..

private static final ConcurrentHashMap<String, HashMap> sharedData;

or

private static final ConcurrentHashMap<String, ConcurrentHashMap> sharedData;

Или было бы безопаснее (но дороже просто clone ())?

this.myCopyOfData = sharedData.get(key).clone();

TIA.

(Статический инициализатор был отредактирован, чтобы дать больше контекста.)


person pschang    schedule 09.03.2010    source источник
comment
См. Соответствующий вопрос: stackoverflow.com/questions/104184/   -  person Dave L.    schedule 01.12.2010


Ответы (8)


ссылка на sharedData, которая является конечной, является поточно-ориентированной, поскольку ее нельзя изменить. Содержимое карты НЕ потокобезопасно, потому что оно должно быть либо заключено в оболочку, предпочтительно ImmutableMap реализацией Guava, либо java.util.Collections.unmodifiableMap(), либо использовать одну из реализаций карты в пакете java.util.concurrent.

Только если вы сделаете ОБА, вы получите полную безопасность потоков на карте. Любые содержащиеся карты должны быть неизменяемыми или одной из параллельных реализаций.

.clone () принципиально не работает, держитесь подальше

клонирование по умолчанию - это неглубокий клон, он просто возвращает ссылки на объекты-контейнеры, а не полные копии. Это хорошо задокументировано в общедоступной информации о том, почему.

person Community    schedule 09.03.2010
comment
@fuzzy lollipop - внутреннее состояние карт (как верхнего уровня, так и находящихся внутри) будет сброшено в основную память после завершения инициализации класса. Защита от последующих изменений с помощью оболочки immutableMap() была бы разумной, но не обязательной для видимости. - person erickson; 09.03.2010
comment
да, но это будет действовать как документация о намерении для будущего сопровождающего, если карта должна быть неизменной. всегда предпочитать явное неявному - person ; 09.03.2010
comment
@ 'fuzzy lollipop' - Вы не можете просто украсть Zen of Python, даже если это правда :) - person extraneon; 09.03.2010
comment
Ах. Я немного упустил. Карты, которые есть в sharedData, также являются окончательными. если бы я сделал это, будет ли это нормально для обеспечения безопасности потоков? - person pschang; 09.03.2010
comment
как карты в окончательной карте? вы имеете в виду неизменный или синхронизированный? final не имеет смысла в том виде, как вы его использовали. - person ; 09.03.2010
comment
Извините. обновил приведенный выше код. не уверен, что его реализация в точности правильна, но в основном содержащиеся карты после инициализации не будут изменены. вся структура доступна только для чтения. - person pschang; 09.03.2010
comment
не будет изменен - ​​это не то же самое, что нельзя изменить, вы должны сделать их неизменяемыми с помощью java.util.Collections.unmodifiableMap (), чтобы их нельзя было точно изменить. Простое выполнение переменной final не делает контейнер потокобезопасным. Это будут делать только методы-оболочки. - person ; 10.03.2010
comment
Я понимаю. Короче говоря, final + unmodifiableMap () = immutability = ›потокобезопасный. - person pschang; 10.03.2010
comment
да. Фактически значение внутренней карты не является строкой. Это объект метода, и да, это становится все сложнее. :\ Прости. Я задал только 2 вопроса, и на последний действительно не было ответа. Спасибо за вашу помощь. - person pschang; 10.03.2010

Инициализация статических полей final в блоке статической инициализации потокобезопасна. Однако помните, что объект, на который указывает статическая конечная ссылка, может не быть потокобезопасным. Если объект, на который вы ссылаетесь, является потокобезопасным (например, неизменяемым), вам ничего не известно.

Не гарантируется, что каждый отдельный HashMap, содержащийся во внешнем HashMap, будет потокобезопасным, если вы не используете ConcurrentHashMap, как предлагается в вашем вопросе. Если вы не используете поточно-ориентированную внутреннюю реализацию HashMap, вы можете получить непредвиденные результаты, когда два потока обращаются к одной и той же внутренней HashMap. Имейте в виду, что синхронизируются только некоторые операции с ConcurrentHashMap. Например, итерация не является потокобезопасной.

person Mike Daniels    schedule 09.03.2010
comment
Для большей наглядности рекомендую изменить доступ к обновлению. Если вы можете гарантировать, что все последующие обращения будут доступны только для чтения с помощью других средств, тогда любой класс является потокобезопасным (таким образом, некоторые обращения являются потокобезопасными, даже если нет блокировки или неизменяемости). Разработка с учетом неизменности упрощает доказательство безопасности потоков, поскольку обеспечивает соблюдение этой гарантии. - person Kevin Brock; 10.03.2010
comment
@freshfunk, безопасно перебирать контейнер, содержимое которого никогда не меняется. Это небезопасно, если содержимое может измениться. - person Mike Daniels; 10.03.2010
comment
Да, это то, что мне подсказывала моя интуиция. В основном потому, что документированные подводные камни, связанные с потокобезопасными объектами, были связаны с изменением содержимого во время использования другими потоками. Но я не был уверен, есть ли внутренние конструкции (такие как текущий указатель индекса во время итерации), которые не были бы потокобезопасными. Например, чтение карты с индексом 5, а затем другой поток, продолжающий свою итерацию, оказывается в неправильном месте. - person pschang; 10.03.2010
comment
@freshfunk, Собственно проблема не в этом. Сами итераторы отслеживают свое положение в коллекции. Проблема в том, что если содержимое коллекции изменяется во время итерации, позиция итератора теряет смысл. Элемент, на который он указывал, мог быть смещен вперед или назад, может быть удален и т. Д. Вот почему большинство стандартных итераторов Java работают без сбоев. Они могут определить, изменилась ли коллекция с момента начала итерации, и выдать исключение ConcurrentModificationException вместо получения неожиданных результатов. - person Mike Daniels; 10.03.2010

Что такое потокобезопасность? Конечно, инициализация HashMap является потокобезопасной в том отношении, что все Foo используют один и тот же экземпляр Map, и что Map гарантированно будет там, если только в статической инициализации не возникнет исключение.

Но изменение содержимого карты, безусловно, небезопасно для потоков. Статический финал означает, что sharedData карты не может быть переключена на другую карту. А вот содержание Карты - это другой вопрос. Если данный ключ используется более одного раза одновременно, могут возникнуть проблемы с параллелизмом.

person extraneon    schedule 09.03.2010

Да, это тоже потокобезопасно. Все конечные члены вашего статического класса будут инициализированы до того, как какой-либо поток получит к ним доступ.

Если блок static выходит из строя во время инициализации, ExceptionInInitializerError будет поднят в потоке, который первым пытается инициализировать. Последующая попытка сослаться на класс вызовет NoClassDefFoundError.

Как правило, содержимое HashMap не гарантирует видимость в потоках. Однако код инициализации класса использует блок synchronized, чтобы предотвратить инициализацию класса несколькими потоками. Эта синхронизация сбрасывает состояние карты (и содержащихся в ней HashMap экземпляров), так что они будут правильно видимы для всех потоков, при условии, что никакие изменения не вносятся в карту или карты, которые она содержит, вне инициализатора класса.

См. Спецификацию языка Java, 12.4.2 для получения информации об инициализации класса и требованиях для синхронизации.

person erickson    schedule 09.03.2010

Нет. За исключением случаев, когда они неизменяемы.

Единственное, что они делают, это

  • Будьте доступными на уровне класса
  • Избегайте изменения ссылки.

Тем не менее, если ваш атрибут изменяемый, он не является потокобезопасным.

См. Также: Синхронизируем ли мы окончательные переменные экземпляров?

Это точно так же, за исключением того, что они классного уровня.

person OscarRyz    schedule 09.03.2010

В переменной final static нет ничего изначально потокобезопасного. Объявление переменной-члена final static только гарантирует, что эта переменная будет назначена только один раз.

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

  • Изменяют ли несколько потоков состояние вашей sharedData переменной?
  • Если да, синхронизируете ли вы все записи (и) sharedData?

Использование ConcurrentHashMap гарантирует только то, что отдельные методы Map являются поточно-ориентированными, оно не выполняет такую ​​операцию, как эта поточно-ориентированная:

if (!map.containsKey("foo")) {
    map.put("foo", bar);
}
person matt b    schedule 09.03.2010
comment
В конечной статической переменной есть что-то изначально потокобезопасное, поскольку final означает, что она может быть назначена только один раз, а присвоение должно происходить во время инициализации класса, которая неявно синхронизируется. Конечно, это ничего не говорит об объектах, на которые ссылается такая переменная, но, например, финальный статический long или double безопасен, тогда как instance или non-final long или double - нет. - person Michael Borgwardt; 09.03.2010
comment
создание чего-либо окончательного по своей сути делает эту переменную потокобезопасной ссылкой - person ; 09.03.2010
comment
Итоговую карту небезопасно совместно использовать среди множества потоков без синхронизации. Это то, что я имею в виду под отсутствием потоковообезопасности в конечной статической переменной. - person matt b; 09.03.2010
comment
@fuzzy lollipop, ссылка может быть потокобезопасной, но как насчет состояния внутри карты? Это главное. - person matt b; 09.03.2010
comment
Пока не будут внесены последующие изменения, это состояние будет синхронизировано: java.sun.com/docs/books/jls/third_edition/html/ - person erickson; 09.03.2010
comment
@erickson, я не имею в виду статический инициализатор, я имею в виду то, что делается с Map (s) после построения объекта. - person matt b; 09.03.2010
comment
Из OP я делаю вывод, что в состояние карты не вносятся никакие изменения за пределами статического инициализатора. Если бы это неправда, у вас была бы проблема с видимостью. - person erickson; 09.03.2010
comment
Да, я прошу прощения за то, что не дал полного контекста моим целям. Редактировал вопрос. Мое намерение состоит в том, чтобы sharedData не изменялась после статического инициализатора. Я хочу также сделать содержащиеся в нем карты поточно-ориентированными. Я изменил приведенный выше код, чтобы указать это. - person pschang; 10.03.2010
comment
@Michael: Формально вы правы. Но на практике содержимое карты по умолчанию не является постоянным. - person extraneon; 10.03.2010

Разве вы не спрашиваете, является ли статическая инициализация sharedData потокобезопасной и выполняется только один раз?

И да, это так.

Конечно, многие здесь правильно указали, что содержимое sharedData все еще может быть изменено.

person Thirler    schedule 09.03.2010
comment
Нет. Я видел множество ответов на этот вопрос. Мой вопрос: когда несколько потоков обращаются к содержимому sharedData, возникнет ли какая-либо конкуренция. - person pschang; 10.03.2010

В этом случае неизменен только объект sharedData, это означает только то, что вы все время будете работать с одним и тем же объектом. Но любые данные внутри него могут быть изменены (удалены, добавлены и т. Д.) В любое время из любого потока.

person Igor Artamonov    schedule 09.03.2010
comment
Это не неизменный. Кажется, это распространенное заблуждение с final, это только предотвращает переназначение sharedData. - person Robin; 09.03.2010
comment
@Robin - он указывает, что любые данные внутри него могут быть изменены ... в любое время. - person erickson; 09.03.2010
comment
Вы правы, это просто плохая формулировка. Очевидно, я не могу отменить свой голос против ... извините! - person Robin; 09.03.2010
comment
извините, английский не мой родной язык :( но я пытаюсь исправить :) - person Igor Artamonov; 10.03.2010