Подтвердить что ты не робот

Java concurrency - мне нужна синхронизация или нет?

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

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

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

Пока все хорошо. Здесь, где мы получаем интерес.

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

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

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

4b9b3361

Ответ 1

Использовать volatile

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

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

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

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

Я рекомендую объявить член validProgramCodes как "изменчивый". Разница в скорости будет незначительной, и это гарантирует безопасность вашего кода сейчас и в будущем, независимо от того, какие оптимизации 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 - это один из способов указать, что это необходимо.

Изменение порядка

Предположим, что следующий код:

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. Карта, лежащая в основе Iterator, испытывает параллельную модификацию и выдает исключение среды выполнения: дьявольски прерывистое, казалось бы, необъяснимое исключение во время выполнения который никогда не производился во время тестирования.

Параллельное программирование

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

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

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

Ответ 2

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

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

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

Ответ 3

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

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

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

Итак, сделать ссылку volatile адекватной в рамках нового JMM. Это вряд ли может существенно повлиять на производительность системы.

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

Ответ 4

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

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

Другой вариант (возможно, более медленный, чем энергозависимый, но 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();
      }
    }
  }

Ответ 5

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

private volatile HashMap validProgramCodes;

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

Ответ 6

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

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

Ответ 7

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

Ответ 9

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

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

Ответ 10

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

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


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

Doug Lea (Параллельное программирование на Java, 2-е изд.), стр. 94, раздел 2.2.7.2 Видимость, позиция № 3:

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

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

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

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

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


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

Я не удаляю ответ в надежде, что это будет полезно. Я не, ищущий значок "Peer Pressure"...; -)