Сценарий параллелизма Java - мне нужна синхронизация или нет? - PullRequest
16 голосов
/ 19 ноября 2008

Вот сделка. У меня есть хэш-карта, содержащая данные, которые я называю «программные коды», они живут в объекте, например:

Class Metadata
{
    private HashMap validProgramCodes;
    public HashMap getValidProgramCodes() { return validProgramCodes; }
    public void setValidProgramCodes(HashMap h) { validProgramCodes = h; }
}

У меня есть множество потоков чтения, каждый из которых будет вызывать getValidProgramCodes () один раз, а затем использовать этот хэш-файл в качестве ресурса только для чтения.

Пока все хорошо. Вот где нам интересно.

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

Моя теория - которую мне нужна помощь для проверки - заключается в том, что я могу продолжать использовать код как есть, без явной синхронизации. Это выглядит так: В то время, когда validProgramCodes обновляется, значение validProgramCodes всегда хорошо - это указатель на новый или старый хэш-файл. Это предположение, на котором все зависит. С читателем, у которого есть старая хэш-карта, все в порядке; он может продолжать использовать старое значение, так как оно не будет собирать мусор, пока не освободит его. Каждый читатель временный; он скоро умрет и будет заменен новым, который поднимет новое значение.

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

Ответы [ 10 ]

28 голосов
/ 19 ноября 2008

Использование летучих

Это тот случай, когда один поток заботится о том, что делает другой? Тогда JMM FAQ имеет ответ:

В большинстве случаев одна нить не все равно, что делает другой. Но когда это делает, это то, что синхронизация для.

В ответ на вопрос тех, кто говорит, что код OP безопасен как есть, рассмотрим следующее: в модели памяти Java нет ничего, что гарантировало бы, что это поле будет сброшено в основную память при запуске нового потока. Кроме того, JVM может свободно переупорядочивать операции, если изменения не обнаруживаются в потоке.

Теоретически, потокам читателей не гарантируется "запись" в validProgramCodes. На практике они в конечном итоге будут, но вы не можете быть уверены, когда.

Я рекомендую объявить элемент validProgramCodes как volatile. Разница в скорости будет незначительной, и она гарантирует безопасность вашего кода сейчас и в будущем, независимо от того, какие оптимизации JVM могут быть введены.

Вот конкретная рекомендация:

import java.util.Collections;

class Metadata {

    private volatile Map validProgramCodes = Collections.emptyMap();

    public Map getValidProgramCodes() { 
      return validProgramCodes; 
    }

    public void setValidProgramCodes(Map h) { 
      if (h == null)
        throw new NullPointerException("validProgramCodes == null");
      validProgramCodes = Collections.unmodifiableMap(new HashMap(h));
    }

}

Неизменность

В дополнение к обертке с unmodifiableMap, я копирую карту (new HashMap(h)). Это делает моментальный снимок, который не изменится, даже если вызывающая программа установщика продолжит обновлять карту "h". Например, они могут очистить карту и добавить новые записи.

Зависит от интерфейсов

На стилистической ноте часто лучше объявлять API с абстрактными типами, такими как List и Map, а не с конкретными типами, такими как ArrayList и HashMap.. Это дает гибкость в будущем, если конкретные типы должны изменить (как я сделал здесь).

Кэширование

Результатом присвоения «h» «validProgramCodes» может быть просто запись в кэш процессора. Даже когда начинается новый поток, «h» не будет виден новому потоку, если он не был сброшен в общую память. Хорошее время выполнения позволит избежать очистки, если в этом нет необходимости, и использование volatile является одним из способов указать, что это необходимо.

1039 * Изменение порядка * Примите следующий код: HashMap codes = new HashMap(); codes.putAll(source); meta.setValidProgramCodes(codes); Если setValidCodes - это просто OP validProgramCodes = h;, компилятор может изменить порядок кода следующим образом: 1: meta.validProgramCodes = codes = new HashMap(); 2: codes.putAll(source); Предположим, что после выполнения строки 1 для записи поток чтения начинает выполнять этот код: 1: Map codes = meta.getValidProgramCodes(); 2: Iterator i = codes.entrySet().iterator(); 3: while (i.hasNext()) { 4: Map.Entry e = (Map.Entry) i.next(); 5: // Do something with e. 6: } Теперь предположим, что поток писателя вызывает «putAll» на карте между строкой 2 и 3 читателя. Карта, лежащая в основе Итератора, претерпела одновременное изменение и выдает исключение во время выполнения - дьявольски прерывистое, казалось бы необъяснимое исключение во время выполнения это никогда не было произведено во время тестирования. Параллельное программирование

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

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

Дополнительные ресурсы

4 голосов
/ 20 ноября 2008

Нет, пример кода небезопасен, поскольку не существует безопасной публикации каких-либо новых экземпляров HashMap. Без какой-либо синхронизации существует вероятность, что поток считывателя увидит частично инициализированный HashMap.

Ознакомьтесь с объяснением @ erickson в разделе «Изменение порядка» в его ответе. Также я не могу рекомендовать книгу Брайана Гетца Java Concurrency на практике достаточно!

Неважно, нормально ли вам, что потоки читателей могут видеть старые (устаревшие) ссылки на HashMap или даже никогда не видеть новую ссылку. Худшее, что может случиться, это то, что поток чтения может получить ссылку и попытаться получить доступ к экземпляру HashMap, который еще не инициализирован и не готов к доступу.

3 голосов
/ 20 ноября 2008

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

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

Другой вариант (возможно, медленнее, чем volatile, но YMMV) - использовать ReentrantReadWriteLock для защиты доступа, чтобы его могли прочитать несколько одновременных читателей. И если это все еще является узким местом в производительности, я съем весь этот веб-сайт.

  public class Metadata
  {
    private HashMap validProgramCodes;
    private ReadWriteLock lock = new ReentrantReadWriteLock();

    public HashMap getValidProgramCodes() { 
      lock.readLock().lock();
      try {
        return validProgramCodes; 
      } finally {
        lock.readLock().unlock();
      }
    }

    public void setValidProgramCodes(HashMap h) { 
      lock.writeLock().lock();
      try {
        validProgramCodes = h; 
      } finally {
        lock.writeLock().unlock();
      }
    }
  }
3 голосов
/ 19 ноября 2008

Нет, согласно модели памяти Java (JMM), это не является потокобезопасным.

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

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

Таким образом, создание ссылки volatile является адекватным для нового JMM. Вряд ли это существенно повлияет на производительность системы.

Мораль этой истории: нить сложна. Не пытайтесь быть умным, потому что иногда (возможно, не в вашей тестовой системе) вы не будете достаточно умны.

2 голосов
/ 19 ноября 2008

Я думаю, что ваши предположения верны. Единственное, что я хотел бы сделать, это установить validProgramCodes volatile.

private volatile HashMap validProgramCodes;

Таким образом, когда вы обновляете «указатель» validProgramCodes, вы гарантируете, что все потоки обращаются к одному и тому же последнему HasMap «указателю», потому что они не полагаются на локальный кэш потоков и переходят непосредственно в память.

1 голос
/ 21 ноября 2008

Хотя это не лучшее решение для этой конкретной проблемы (идея Эриксона о новой не изменяемой карте), я хотел бы на минутку упомянуть представленный класс java.util.concurrent.ConcurrentHashMap в Java 5 - версия HashMap, специально созданная с учетом параллелизма. Эта конструкция не блокирует чтение.

1 голос
/ 19 ноября 2008

Назначение будет работать до тех пор, пока вы не будете беспокоиться о чтении устаревших значений, и до тех пор, пока вы можете гарантировать, что ваша хэш-карта будет правильно заполнена при инициализации. По крайней мере, вы должны создать hashMap с Collections.unmodifiableMap на Hashmap, чтобы гарантировать, что ваши читатели не будут изменять / удалять объекты с карты, и чтобы избежать множественных потоков, наступающих на пальцы друг друга, и аннулирующих итераторов при разрушении других потоков.

(автор выше прав насчет изменчивости, должен был это видеть)

0 голосов
/ 28 августа 2013

Проверьте этот пост об основах параллелизма. Он должен быть в состоянии ответить на ваш вопрос удовлетворительно.

http://walivi.wordpress.com/2013/08/24/concurrency-in-java-a-beginners-introduction/

0 голосов
/ 19 ноября 2008

Если я правильно прочитал JLS (никаких гарантий!), Доступ к ссылкам всегда атомарный, точка. См. Раздел 17.7 Неатомарная обработка двойной и длинной

Таким образом, если доступ к ссылке всегда атомарный и не имеет значения, какой экземпляр возвращенного Hashmap потока видят, у вас все должно быть в порядке. Вы никогда не увидите частичные записи в ссылку.


Изменить: После просмотра обсуждения в комментариях ниже и других ответов, здесь приведены ссылки / цитаты из

Книга Дуга Ли (Параллельное программирование на Java, 2-е издание), с. 94, раздел 2.2.7.2 Видимость , элемент № 3: "

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

на стр. 94, Ли продолжает описывать риски, связанные с этим подходом:

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

Таким образом, когда он абсолютно, положительно, должен быть видимым для любого вызывающего потока, volatile или какой-либо другой барьер синхронизации требуется, особенно в долго работающих потоках или потоках, которые обращаются значение в цикле (как говорит Ли).

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


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

Я не удаляю ответ в надежде, что он будет полезен. Я не ищу значок "Давление сверстников" ...; -)

0 голосов
/ 19 ноября 2008

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

По крайней мере, я бы также объявил validProgramCodes равным volatile, чтобы ссылка не была оптимизирована в регистр или что-то в этом роде.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...