Синхронизация объектов String в Java

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

После реализации этого базового кеширования в ходе дальнейшего тестирования я обнаружил, что не учел, как параллельные потоки могут одновременно обращаться к кешу. Я обнаружил, что в течение ~ 100 мс около 50 потоков пытались получить объект из кеша, обнаружив, что срок его действия истек, обращались к веб-службе для получения данных, а затем помещали объект обратно в кеш.

Исходный код выглядел примерно так:

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {

  final String key = "Data-" + email;
  SomeData[] data = (SomeData[]) StaticCache.get(key);

  if (data == null) {
      data = service.getSomeDataForEmail(email);

      StaticCache.set(key, data, CACHE_TIME);
  }
  else {
      logger.debug("getSomeDataForEmail: using cached object");
  }

  return data;
}

Итак, чтобы убедиться, что только один поток вызывал веб-службу, когда срок действия объекта в key истек, я подумал, что мне нужно синхронизировать операцию получения / установки кэша, и казалось, что использование ключа кеша будет хорошим кандидатом для объекта для синхронизации (таким образом, вызовы этого метода для электронной почты [email protected] не будут блокироваться вызовами метода для [email protected]).

Я обновил метод, чтобы он выглядел так:

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {

  
  SomeData[] data = null;
  final String key = "Data-" + email;
  
  synchronized(key) {      
    data =(SomeData[]) StaticCache.get(key);

    if (data == null) {
        data = service.getSomeDataForEmail(email);
        StaticCache.set(key, data, CACHE_TIME);
    }
    else {
      logger.debug("getSomeDataForEmail: using cached object");
    }
  }

  return data;
}

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

Однако похоже, что это не сработало. Мои тестовые журналы имеют такой вывод:

(log output is 'threadname' 'logger name' 'message')  
http-80-Processor253 jsp.view-page - getSomeDataForEmail: about to enter synchronization block  
http-80-Processor253 jsp.view-page - getSomeDataForEmail: inside synchronization block  
http-80-Processor253 cache.StaticCache - get: object at key [[email protected]] has expired  
http-80-Processor253 cache.StaticCache - get: key [[email protected]] returning value [null]  
http-80-Processor263 jsp.view-page - getSomeDataForEmail: about to enter synchronization block  
http-80-Processor263 jsp.view-page - getSomeDataForEmail: inside synchronization block  
http-80-Processor263 cache.StaticCache - get: object at key [[email protected]] has expired  
http-80-Processor263 cache.StaticCache - get: key [[email protected]] returning value [null]  
http-80-Processor131 jsp.view-page - getSomeDataForEmail: about to enter synchronization block  
http-80-Processor131 jsp.view-page - getSomeDataForEmail: inside synchronization block  
http-80-Processor131 cache.StaticCache - get: object at key [[email protected]] has expired  
http-80-Processor131 cache.StaticCache - get: key [[email protected]] returning value [null]  
http-80-Processor104 jsp.view-page - getSomeDataForEmail: inside synchronization block  
http-80-Processor104 cache.StaticCache - get: object at key [[email protected]] has expired  
http-80-Processor104 cache.StaticCache - get: key [[email protected]] returning value [null]  
http-80-Processor252 jsp.view-page - getSomeDataForEmail: about to enter synchronization block  
http-80-Processor283 jsp.view-page - getSomeDataForEmail: about to enter synchronization block  
http-80-Processor2 jsp.view-page - getSomeDataForEmail: about to enter synchronization block  
http-80-Processor2 jsp.view-page - getSomeDataForEmail: inside synchronization block  

Я хотел видеть только один поток за раз, входящий / выходящий из блока синхронизации при операциях получения / установки.

Есть ли проблема с синхронизацией объектов String? Я думал, что ключ кеша будет хорошим выбором, поскольку он уникален для операции, и хотя final String key объявлен внутри метода, я думал, что каждый поток будет получать ссылку на тот же объект и, следовательно, будет синхронизироваться на этом единственном объект.

Что я здесь делаю не так?

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

final String key = "blah";
...
synchronized(key) { ...

не проявляют той же проблемы параллелизма - только один поток одновременно входит в блок.

Обновление 2: Всем спасибо за помощь! Я принял первый ответ о intern()ing Strings, который решил мою первоначальную проблему - когда несколько потоков вводили синхронизированные блоки, где я думал, что они не должны, потому что key имеет одинаковое значение.

Как отмечали другие, использование intern() для такой цели и синхронизация этих строк действительно оказалось плохой идеей - при запуске тестов JMeter против веб-приложения для имитации ожидаемой нагрузки я увидел, что размер используемой кучи вырос почти до 1 ГБ чуть менее чем за 20 минут.

В настоящее время я использую простое решение - просто синхронизировать весь метод - но мне В самом деле нравятся образцы кода, предоставленные martinprobst и MBCook, но поскольку в настоящее время у меня есть около 7 похожих методов getData() в этом классе (поскольку для этого требуется около 7 различных частей данные из веб-службы), я не хотел добавлять почти повторяющуюся логику получения и снятия блокировок для каждого метода. Но это определенно очень ценная информация для будущего использования. Я думаю, что это, в конечном счете, правильные ответы о том, как лучше всего сделать такую ​​операцию потокобезопасной, и я бы отдал больше голосов за эти ответы, если бы мог!

Вам больше не нужно беспокоиться о том, что внутренняя строка зависает в памяти - по всей видимости, внутренние строки уже довольно давно хранятся в GC: stackoverflow.com/questions/18152560/…

Volksman 16.02.2017 20:07

Я рекомендую этот ответ, используя полосатый <Lock> Гуавы, чтобы избежать чрезмерного использования памяти: stackoverflow.com/a/11125602/116810

Kimball Robinson 24.02.2018 03:09
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
50
2
47 868
21
Перейти к ответу Данный вопрос помечен как решенный

Ответы 21

Ответ принят как подходящий

Не вдаваясь в подробности, при быстром просмотре того, что вы говорите, похоже, что вам нужно intern () ваши строки:

final String firstkey = "Data-" + email;
final String key = firstkey.intern();

Две строки с одинаковым значением в противном случае не обязательно являются одним и тем же объектом.

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

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

Ответ на обновление вопроса:

Я думаю, это потому, что строковый литерал всегда дает один и тот же объект. Дэйв Коста отмечает в комментарии, что это даже лучше: литерал всегда дает каноническое представление. Таким образом, все строковые литералы с одинаковым значением в любом месте программы будут давать один и тот же объект.

Редактировать

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

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

Вот альтернатива - он по-прежнему использует одиночную блокировку, но мы знаем, что нам в любом случае понадобится одна из них для кеша, и вы говорили о 50 потоках, а не о 5000, так что это не может быть фатальным. Я также предполагаю, что узким местом производительности здесь является медленный блокирующий ввод-вывод в DoSlowThing (), который, следовательно, получит огромную выгоду, если не будет сериализован. Если проблема не в этом, то:

  • Если ЦП занят, этого подхода может быть недостаточно, и вам нужен другой подход.
  • Если ЦП не занят и доступ к серверу не является узким местом, тогда этот подход является излишним, и вы можете также забыть и об этом, и о блокировке по ключам, поместить большой синхронизированный (StaticCache) вокруг всей операции и это легкий способ.

Очевидно, что этот подход необходимо протестировать на масштабируемость перед использованием - я ничего не гарантирую.

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

IN_PROGRESS - фиктивное значение - не совсем чистое, но код простой и позволяет избежать двух хэш-таблиц. Он не обрабатывает InterruptedException, потому что я не знаю, что ваше приложение хочет делать в этом случае. Кроме того, если DoSlowThing () постоянно терпит неудачу для данного ключа, этот код в его нынешнем виде не совсем элегантен, поскольку каждый проходящий поток будет повторять его. Поскольку я не знаю, каковы критерии отказа и могут ли они быть временными или постоянными, я тоже не занимаюсь этим, я просто проверяю, не блокируются ли потоки навсегда. На практике вы можете захотеть поместить значение данных в кеш, которое указывает «недоступно», возможно, по причине, и тайм-аут, когда следует повторить попытку.

// do not attempt double-check locking here. I mean it.
synchronized(StaticObject) {
    data = StaticCache.get(key);
    while (data == IN_PROGRESS) {
        // another thread is getting the data
        StaticObject.wait();
        data = StaticCache.get(key);
    }
    if (data == null) {
        // we must get the data
        StaticCache.put(key, IN_PROGRESS, TIME_MAX_VALUE);
    }
}
if (data == null) {
    // we must get the data
    try {
        data = server.DoSlowThing(key);
    } finally {
        synchronized(StaticObject) {
            // WARNING: failure here is fatal, and must be allowed to terminate
            // the app or else waiters will be left forever. Choose a suitable
            // collection type in which replacing the value for a key is guaranteed.
            StaticCache.put(key, data, CURRENT_TIME);
            StaticObject.notifyAll();
        }
    }
}

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

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

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

Dave Costa 25.09.2008 19:39

Ах, это отвечает на мою расплывчатую болтовню «Я не могу вспомнить». Спасибо.

Steve Jessop 25.09.2008 19:42

Вызов String.intern () на веб-сервере, где кеш String может существовать бесконечно, - не лучшая идея. Я не говорю, что этот пост неверен, но это не лучшее решение исходного вопроса.

McDowell 25.09.2008 20:23

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

Steve Jessop 25.09.2008 20:35

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

Martin Probst 26.09.2008 12:23

Просто примечание, чтобы указать, что синхронизация строки ВСЕГДА является плохой идеей: ничто не удерживает случайную стороннюю банку на вашем пути к классам от синхронизации с той же интернированной строкой. Это ВСЕГДА приведет к нежелательному поведению.

Jared 13.02.2009 21:05

Ну, вы не можете поместить случайные сторонние jar-файлы в свой путь к классам ;-). Но согласился. Мой первоначальный ответ на вопрос был типа «ответить на вопрос, но не заметить совершенную ужасную ошибку». Мое кодовое решение позволяет избежать проблем.

Steve Jessop 21.02.2009 06:03

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

Holger 05.09.2016 20:55

@Holger: поскольку я написал, что «создание внутренних строк разрешено, чтобы они существовали вечно», я пришел к выводу, что в то время я думал, что нет гарантировано спецификацией виртуальной машины, что интернированные строки обычно собираются с помощью GC. В любом случае конкретная реализация может это сделать, и, вероятно, должна.

Steve Jessop 05.09.2016 20:58

@ Стив Джессоп: Я бы посмотрел на это с точностью до наоборот, пока в спецификации нет явного заявления об этом, нет причин думать, что определенные объекты могут быть освобождены от сборки мусора. Но что более важно, я думаю, опасно называть что-то в качестве первой (или основной) причины, которая не является проблемой для обычных / реальных JVM. Другие причины в том, что JVM независимо, вещь ...

Holger 05.09.2016 21:14

Я рекомендую этот ответ, используя тип Google Guava Striped <Lock>: stackoverflow.com/a/11125602/116810

Kimball Robinson 24.02.2018 02:46

Мне нравится этот ответ: stackoverflow.com/questions/133988/…

Bret Royster 17.06.2020 03:49

Вызов:

   final String key = "Data-" + email;

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

Это дальнейшее объяснение вашего редактирования. Когда у вас есть статическая строка, она будет работать.

Использование intern () решает проблему, потому что оно возвращает строку из внутреннего пула, хранимого классом String, что гарантирует, что если две строки равны, будет использоваться строка из пула. Видеть

http://java.sun.com/j2se/1.4.2/docs/api/java/lang/String.html#intern ()

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

Это решается путем использования, например, ConcurrentHashmap.

Steve Jessop 25.09.2008 20:08

Вы правы, я забыл про ConcurrentHashmap! Однако, судя по обновлению вопроса, StaticCache не поддерживает одновременную модификацию.

Alexander 25.09.2008 20:37

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

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

Например.:

Object data = StaticCache.get(key, ...);
if (data == null) {
  Object lock = lockTable.get(key);
  if (lock == null) {
    // we're the only one looking for this
    lock = new Object();
    synchronized(lock) {
      lockTable.put(key, lock);
      // get stuff
      lockTable.remove(key);
    }
  } else {
    synchronized(lock) {
      // just to wait for the updater
    }
    data = StaticCache.get(key);
  }
} else {
  // use from cache
}

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

Если вы делаете кеш недействительным через некоторое время, вы должны проверить, являются ли данные снова нулевыми после извлечения их из кеша в случае lock! = Null.

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

Вопрос - вы имеете в виду, если я синхронизируюсь по ОДНОЙ интернированной строке в другом месте приложения?

matt b 25.09.2008 19:59

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

Outlaw Programmer 25.09.2008 20:06

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

Steve Jessop 25.09.2008 20:16

Похоже, в этом примере есть проблема, когда доступ к lockTable не синхронизирован, что в основном может иметь как минимум два плохих последствия: (1) вы можете в конечном итоге выполнить несколько одновременных операций над StaticCache для одного и того же ключа, (2) разные состояния таблицы lockTable, которую видят разные процессоры.

Alexander 25.09.2008 20:41

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

Outlaw Programmer 25.09.2008 20:54

StaticCache от вопроса.

Michael Myers 25.09.2008 22:15

Взгляните на правильно синхронизированную таблицу блокировок в мой ответ.

Vadzim 12.11.2017 01:35

Почему бы просто не отобразить статическую html-страницу, которая обслуживается пользователем и восстанавливается каждые x минут?

Другие предлагали интернировать струны, и это сработает.

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

Я видел два решения этого:

Вы можете синхронизировать на другом объекте

Вместо электронного письма создайте объект, содержащий электронное письмо (скажем, объект «Пользователь»), который содержит значение электронной почты в качестве переменной. Если у вас уже есть другой объект, представляющий человека (скажем, вы уже вытащили что-то из БД на основе его электронной почты), вы можете его использовать. Реализуя метод equals и метод хэш-кода, вы можете убедиться, что Java считает объекты одинаковыми, когда вы выполняете статический cache.contains (), чтобы узнать, находятся ли данные уже в кеше (вам нужно будет синхронизировать в кеше ).

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

Map<String, Object> emailLocks = new HashMap<String, Object>();

Object lock = null;

synchronized (emailLocks) {
    lock = emailLocks.get(emailAddress);

    if (lock == null) {
        lock = new Object();
        emailLocks.put(emailAddress, lock);
    }
}

synchronized (lock) {
    // See if this email is in the cache
    // If so, serve that
    // If not, generate the data

    // Since each of this person's threads synchronizes on this, they won't run
    // over eachother. Since this lock is only for this person, it won't effect
    // other people. The other synchronized block (on emailLocks) is small enough
    // it shouldn't cause a performance problem.
}

Это предотвратит получение 15 запросов на один и тот же адрес электронной почты за один раз. Вам понадобится что-то, чтобы предотвратить попадание слишком большого количества записей на карту emailLocks. Это можно сделать с помощью LRUMaps из Apache Commons.

Это потребует некоторой настройки, но это может решить вашу проблему.

Используйте другой ключ

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

Резюме

Надеюсь, это поможет. Распределение потоков - это весело, не так ли? Вы также можете использовать сеанс, чтобы установить значение, означающее «Я уже работаю над поиском этого», и проверить это, чтобы увидеть, нужно ли второму (третьему, N-му) потоку попытаться создать или просто дождаться появления результата. в кеше. Думаю, у меня было три предложения.

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

McDowell 25.09.2008 20:21

«ints не нужно интернировать» - но у них нет мониторов. Ваше резюме намекает на захватывающий мир wait / notifyAll, который всегда вызывает смех :-)

Steve Jessop 25.09.2008 20:24

@McDowell: Думаю, WeakHashMap здесь действительно подойдет. Однако это настолько редкое явление, что я не решаюсь сказать больше ...

Steve Jessop 25.09.2008 20:32

@McDowell: Вот почему я сказал LRUMap в комментариях, это помешает вам когда-либо иметь более 30 записей. Вы также можете настроить поток для этого.

MBCook 25.09.2008 22:19

@onebyone: Хорошее замечание. Целые числа есть, но целое число не равно целому числу, например int, вы должны использовать .intValue () или .equals (). Как я сказал, это были быстрые предложения. Я бы это заметил, когда заметил, что код не работает :)

MBCook 25.09.2008 22:20

Думаю, я переработал ваше третье предложение в решение для приложений со средней нагрузкой. Код удивительно похож на то время, когда я когда-то реализовал pthread_once () на C во встроенной реализации части POSIX. Так что искренне надеюсь, что не глючит ...

Steve Jessop 26.09.2008 03:40

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

Steve Jessop 26.09.2008 03:43

ConcurrentHashMap может использоваться для отображения блокировок, чтобы избежать первой секции synchronized, как в мой ответ.

Vadzim 12.11.2017 01:17

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

Также:

  • Я надеюсь, что методы StaticCache.get () и набор() безопасны для потоков.
  • String.intern () имеет свою стоимость (которая зависит от реализации виртуальной машины) и должна использоваться с осторожностью.

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

final String key = "Data-" + email;

Есть ли в кеше другие объекты / типы объектов, которые используют адрес электронной почты, который вам нужен, эти дополнительные «Data-» в начале ключа?

если нет, я бы просто сделал это

final String key = email;

и вы также избегаете создания лишних строк.

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

 private ConcurrentMap<String, Future<SomeData[]> cache;
 private SomeData[] getSomeDataByEmail(final WebServiceInterface service, final String email) throws Exception {

  final String key = "Data-" + email;
  Callable<SomeData[]> call = new Callable<SomeData[]>() {
      public SomeData[] call() {
          return service.getSomeDataForEmail(email);
      }
  }
  FutureTask<SomeData[]> ft; ;
  Future<SomeData[]> f = cache.putIfAbsent(key, ft= new FutureTask<SomeData[]>(call)); //atomic
  if (f == null) { //this means that the cache had no mapping for the key
      f = ft;
      ft.run();
  }
  return f.get(); //wait on the result being available if it is being calculated in another thread
}

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

Мне очень нравится это решение - нет необходимости хранить отдельный незавершенный дозорный или отдельную карту ключей для замков - оно либо содержит Future (другой поток работает над ним, просто вызовите get), либо нет (никакой другой потоки, работающие на нем, делаем дорогостоящий расчет)

Krease 02.11.2016 04:17

Используйте достойный фреймворк кеширования, такой как ehcache.

Реализовать хороший кеш не так просто, как думают некоторые.

Что касается комментария о том, что String.intern () является источником утечек памяти, это на самом деле неверно. Собран мусор Interned Strings являются, это может занять больше времени, потому что на некоторых JVM (SUN) они хранятся в пермском пространстве, которое затрагивается только полными сборщиками мусора.

Это довольно поздно, но здесь представлено довольно много некорректного кода.

В этом примере:

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {


  SomeData[] data = null;
  final String key = "Data-" + email;

  synchronized(key) {      
    data =(SomeData[]) StaticCache.get(key);

    if (data == null) {
        data = service.getSomeDataForEmail(email);
        StaticCache.set(key, data, CACHE_TIME);
    }
    else {
      logger.debug("getSomeDataForEmail: using cached object");
    }
  }

  return data;
}

Неправильная область синхронизации. Для статического кеша, поддерживающего API получения / ввода, должна быть как минимум синхронизация операций типа get и getIfAbsentPut для безопасного доступа к кешу. Объем синхронизации будет сам кеш.

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

SynchronizedMap можно использовать вместо явной синхронизации, но при этом необходимо соблюдать осторожность. Если используются неправильные API (get и put вместо putIfAbsent), то операции не будут иметь необходимой синхронизации, несмотря на использование синхронизированной карты. Обратите внимание на сложности, возникающие при использовании putIfAbsent: либо значение put должно вычисляться даже в тех случаях, когда оно не требуется (потому что функция put не может знать, требуется ли значение put, пока не будет исследовано содержимое кеша), либо требуется тщательная проверка. использование делегирования (скажем, с использованием Future, которое работает, но в некоторой степени несовместимо; см. ниже), при котором значение пут получается по запросу, если это необходимо.

Использование Futures возможно, но кажется довольно неудобным и, возможно, немного изощренным. Future API лежит в основе асинхронных операций, в частности, для операций, которые могут не завершиться немедленно. Вовлечение Future, скорее всего, добавляет уровень создания потоков - дополнительные, вероятно, ненужные сложности.

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

другой способ синхронизации на строковом объекте:

String cacheKey = ...;

    Object obj = cache.get(cacheKey)

    if (obj==null){
    synchronized (Integer.valueOf(Math.abs(cacheKey.hashCode()) % 127)){
          obj = cache.get(cacheKey)
         if (obj==null){
             //some cal obtain obj value,and put into cache
        }
    }
}

У него те же недостатки, что и у String.intern, с более высокой вероятностью попадания в беду.

Vadzim 12.11.2017 00:41

Вот безопасное короткое решение для Java 8, которое использует карту выделенных объектов блокировки для синхронизации:

private static final Map<String, Object> keyLocks = new ConcurrentHashMap<>();

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {
    final String key = "Data-" + email;
    synchronized (keyLocks.computeIfAbsent(key, k -> new Object())) {
        SomeData[] data = StaticCache.get(key);
        if (data == null) {
            data = service.getSomeDataForEmail(email);
            StaticCache.set(key, data);
        }
    }
    return data;
}

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

Это можно обойти следующим образом:

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {
    final String key = "Data-" + email;
    synchronized (keyLocks.computeIfAbsent(key, k -> new Object())) {
        try {
            SomeData[] data = StaticCache.get(key);
            if (data == null) {
                data = service.getSomeDataForEmail(email);
                StaticCache.set(key, data);
            }
        } finally {
            keyLocks.remove(key); // vulnerable to race-conditions
        }
    }
    return data;
}

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

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

Так что может быть более безопасно и эффективно использовать истекающий срок действия кэша Guava:

private static final LoadingCache<String, Object> keyLocks = CacheBuilder.newBuilder()
        .expireAfterAccess(10, TimeUnit.MINUTES) // max lock time ever expected
        .build(CacheLoader.from(Object::new));

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {
    final String key = "Data-" + email;
    synchronized (keyLocks.getUnchecked(key)) {
        SomeData[] data = StaticCache.get(key);
        if (data == null) {
            data = service.getSomeDataForEmail(email);
            StaticCache.set(key, data);
        }
    }
    return data;
}

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

Теоретически вы можете использовать вычисленный ключ как объект блокировки, тогда вы избежите перераспределения новых объектов (ключ должен быть вычислен в любом случае). Если ConcurrentHashMap используется для keyLocks, это не должно оставлять места для условий гонки? computeIfAbsent вставит только первый, более поздние попытки вернут идентичный экземпляр?

knittl 30.12.2018 13:16

@knittl, да, но без intern() он был бы ненадежным. И он будет страдать от всех недостатков, упомянутых в отвечать Мартина Пробста здесь.

Vadzim 30.12.2018 20:35

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

knittl 31.12.2018 13:22

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

Vadzim 02.01.2019 00:18

Вам не нужны одинаковые предметы. Вы используете объединенные строки в качестве ключа в хэш-карте. Они обеспечивают равенство хешей и значений, поэтому вы найдете правильное значение для одинаковых ключей. Но иметь автоматическое интернирование струн - это хороший момент! (Я сомневаюсь в этом, потому что испускаемый байт-код обычно является обычным построителем строк (или причудливым динамическим кодом в новых версиях JDK))

knittl 02.01.2019 00:43

@knittl, теперь я понял суть первого комментария использования keyLocks.computeIfAbsent(key, key). Но я думаю, что мой второй фрагмент все еще будет уязвим для условий гонки, поскольку keyLocks.remove другого потока может пробираться между computeIfAbsent и содержащим synchronized.

Vadzim 02.01.2019 02:02

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

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;

public class KeySynchronizer<T> {

    private Map<T, CounterLock> locks = new ConcurrentHashMap<>();

    public <U> U synchronize(T key, Supplier<U> supplier) {
        CounterLock lock = locks.compute(key, (k, v) -> 
                v == null ? new CounterLock() : v.increment());
        synchronized (lock) {
            try {
                return supplier.get();
            } finally {
                if (lock.decrement() == 0) {
                    // Only removes if key still points to the same value,
                    // to avoid issue described below.
                    locks.remove(key, lock);
                }
            }
        }
    }

    private static final class CounterLock {

        private AtomicInteger remaining = new AtomicInteger(1);

        private CounterLock increment() {
            // Returning a new CounterLock object if remaining = 0 to ensure that
            // the lock is not removed in step 5 of the following execution sequence:
            // 1) Thread 1 obtains a new CounterLock object from locks.compute (after evaluating "v == null" to true)
            // 2) Thread 2 evaluates "v == null" to false in locks.compute
            // 3) Thread 1 calls lock.decrement() which sets remaining = 0
            // 4) Thread 2 calls v.increment() in locks.compute
            // 5) Thread 1 calls locks.remove(key, lock)
            return remaining.getAndIncrement() == 0 ? new CounterLock() : this;
        }

        private int decrement() {
            return remaining.decrementAndGet();
        }
    }
}

В случае OP это будет использоваться так:

private KeySynchronizer<String> keySynchronizer = new KeySynchronizer<>();

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {
    String key = "Data-" + email;
    return keySynchronizer.synchronize(key, () -> {
        SomeData[] existing = (SomeData[]) StaticCache.get(key);
        if (existing == null) {
            SomeData[] data = service.getSomeDataForEmail(email);
            StaticCache.set(key, data, CACHE_TIME);
            return data;
        }
        logger.debug("getSomeDataForEmail: using cached object");
        return existing;
    });
}

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

public void synchronize(T key, Runnable runnable) {
    CounterLock lock = locks.compute(key, (k, v) -> 
            v == null ? new CounterLock() : v.increment());
    synchronized (lock) {
        try {
            runnable.run();
        } finally {
            if (lock.decrement() == 0) {
                // Only removes if key still points to the same value,
                // to avoid issue described below.
                locks.remove(key, lock);
            }
        }
    }
}

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

public class ValueLock<T> {

    private Lock lock = new ReentrantLock();
    private Map<T, Condition> conditions  = new HashMap<T, Condition>();

    public void lock(T t){
        lock.lock();
        try {
            while (conditions.containsKey(t)){
                conditions.get(t).awaitUninterruptibly();
            }
            conditions.put(t, lock.newCondition());
        } finally {
            lock.unlock();
        }
    }

    public void unlock(T t){
        lock.lock();
        try {
            Condition condition = conditions.get(t);
            if (condition == null)
                throw new IllegalStateException();// possibly an attempt to release what wasn't acquired
            conditions.remove(t);
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

После (внешней) операции lock (внутренняя) блокировка приобретается для получения монопольного доступа к карте на короткое время, и если соответствующий объект уже находится на карте, текущий поток будет ждать, в противном случае он поместит новый Condition на карту, снимет (внутреннюю) блокировку и продолжит, и (внешняя) блокировка считается полученной. Операция (внешняя) unlock, сначала установив (внутреннюю) блокировку, отправит сигнал на Condition, а затем удалит объект с карты.

Класс не использует параллельную версию Map, потому что каждый доступ к нему защищен одиночной (внутренней) блокировкой.

Обратите внимание, семантика метода lock() этого класса отличается от семантики ReentrantLock.lock(), повторные вызовы lock() без парного unlock() будут зависать в текущем потоке на неопределенное время.

Пример использования, который может быть применим к ситуации, описанной OP

    ValueLock<String> lock = new ValueLock<String>();
    // ... share the lock   
    String email = "...";
    try {
        lock.lock(email);
        //... 
    } finally {
        lock.unlock(email);
    }

Меня очень интересует ваш объект ValueLock, но он меня смущает. Как я это читал, если поток 2 вызывает `lock ()`, в то время как Thread1 уже что-то делает, он будет ждать на conditions.awaitUninteruptibly(). Это заблокирует T1 от вызова метода unlock(), поскольку ReentrantLock удерживается T2 в этот момент. Я что-то неправильно читаю?

Eric B. 28.11.2018 02:22

@Eric B, Ни один из ваших потоков 1 или 2 не имеет внутренней (реентерабельной) блокировки, за исключением очень короткого периода времени при работе с картой Value-to-Condition. Когда поток 1 удерживает блокировку значения, это означает, что он удерживает блокировку определенного условия привязки значения, а не внутренней (повторной) блокировки. Когда поток 2 непрерывно ожидает выполнения этого условия привязки к значению, он фактически освобождает внутреннюю (реентерабельную) блокировку. Пока он не получит это Condition, затем получит внутреннюю (Reentrant) Lock, поместит Condition в карту и немедленно освободит внутреннюю (Reentrant) Lock. Пожалуйста, дайте мне знать, если мне придется перефразировать это

igor.zh 29.11.2018 22:28

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

См. Реализацию для Java 8, Java 6 и небольшой тест.

Java 8:

public class DynamicKeyLock<T> implements Lock
{
    private final static ConcurrentHashMap<Object, LockAndCounter> locksMap = new ConcurrentHashMap<>();

    private final T key;

    public DynamicKeyLock(T lockKey)
    {
        this.key = lockKey;
    }

    private static class LockAndCounter
    {
        private final Lock lock = new ReentrantLock();
        private final AtomicInteger counter = new AtomicInteger(0);
    }

    private LockAndCounter getLock()
    {
        return locksMap.compute(key, (key, lockAndCounterInner) ->
        {
            if (lockAndCounterInner == null) {
                lockAndCounterInner = new LockAndCounter();
            }
            lockAndCounterInner.counter.incrementAndGet();
            return lockAndCounterInner;
        });
    }

    private void cleanupLock(LockAndCounter lockAndCounterOuter)
    {
        if (lockAndCounterOuter.counter.decrementAndGet() == 0)
        {
            locksMap.compute(key, (key, lockAndCounterInner) ->
            {
                if (lockAndCounterInner == null || lockAndCounterInner.counter.get() == 0) {
                    return null;
                }
                return lockAndCounterInner;
            });
        }
    }

    @Override
    public void lock()
    {
        LockAndCounter lockAndCounter = getLock();

        lockAndCounter.lock.lock();
    }

    @Override
    public void unlock()
    {
        LockAndCounter lockAndCounter = locksMap.get(key);
        lockAndCounter.lock.unlock();

        cleanupLock(lockAndCounter);
    }


    @Override
    public void lockInterruptibly() throws InterruptedException
    {
        LockAndCounter lockAndCounter = getLock();

        try
        {
            lockAndCounter.lock.lockInterruptibly();
        }
        catch (InterruptedException e)
        {
            cleanupLock(lockAndCounter);
            throw e;
        }
    }

    @Override
    public boolean tryLock()
    {
        LockAndCounter lockAndCounter = getLock();

        boolean acquired = lockAndCounter.lock.tryLock();

        if (!acquired)
        {
            cleanupLock(lockAndCounter);
        }

        return acquired;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException
    {
        LockAndCounter lockAndCounter = getLock();

        boolean acquired;
        try
        {
            acquired = lockAndCounter.lock.tryLock(time, unit);
        }
        catch (InterruptedException e)
        {
            cleanupLock(lockAndCounter);
            throw e;
        }

        if (!acquired)
        {
            cleanupLock(lockAndCounter);
        }

        return acquired;
    }

    @Override
    public Condition newCondition()
    {
        LockAndCounter lockAndCounter = locksMap.get(key);

        return lockAndCounter.lock.newCondition();
    }
}

Java 6:

открытый класс DynamicKeyLock реализует Lock { частный финальный статический ConcurrentHashMap locksMap = new ConcurrentHashMap (); закрытый конечный ключ T;

    public DynamicKeyLock(T lockKey) {
        this.key = lockKey;
    }

    private static class LockAndCounter {
        private final Lock lock = new ReentrantLock();
        private final AtomicInteger counter = new AtomicInteger(0);
    }

    private LockAndCounter getLock()
    {
        while (true) // Try to init lock
        {
            LockAndCounter lockAndCounter = locksMap.get(key);

            if (lockAndCounter == null)
            {
                LockAndCounter newLock = new LockAndCounter();
                lockAndCounter = locksMap.putIfAbsent(key, newLock);

                if (lockAndCounter == null)
                {
                    lockAndCounter = newLock;
                }
            }

            lockAndCounter.counter.incrementAndGet();

            synchronized (lockAndCounter)
            {
                LockAndCounter lastLockAndCounter = locksMap.get(key);
                if (lockAndCounter == lastLockAndCounter)
                {
                    return lockAndCounter;
                }
                // else some other thread beat us to it, thus try again.
            }
        }
    }

    private void cleanupLock(LockAndCounter lockAndCounter)
    {
        if (lockAndCounter.counter.decrementAndGet() == 0)
        {
            synchronized (lockAndCounter)
            {
                if (lockAndCounter.counter.get() == 0)
                {
                    locksMap.remove(key);
                }
            }
        }
    }

    @Override
    public void lock()
    {
        LockAndCounter lockAndCounter = getLock();

        lockAndCounter.lock.lock();
    }

    @Override
    public void unlock()
    {
        LockAndCounter lockAndCounter = locksMap.get(key);
        lockAndCounter.lock.unlock();

        cleanupLock(lockAndCounter);
    }


    @Override
    public void lockInterruptibly() throws InterruptedException
    {
        LockAndCounter lockAndCounter = getLock();

        try
        {
            lockAndCounter.lock.lockInterruptibly();
        }
        catch (InterruptedException e)
        {
            cleanupLock(lockAndCounter);
            throw e;
        }
    }

    @Override
    public boolean tryLock()
    {
        LockAndCounter lockAndCounter = getLock();

        boolean acquired = lockAndCounter.lock.tryLock();

        if (!acquired)
        {
            cleanupLock(lockAndCounter);
        }

        return acquired;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException
    {
        LockAndCounter lockAndCounter = getLock();

        boolean acquired;
        try
        {
            acquired = lockAndCounter.lock.tryLock(time, unit);
        }
        catch (InterruptedException e)
        {
            cleanupLock(lockAndCounter);
            throw e;
        }

        if (!acquired)
        {
            cleanupLock(lockAndCounter);
        }

        return acquired;
    }

    @Override
    public Condition newCondition()
    {
        LockAndCounter lockAndCounter = locksMap.get(key);

        return lockAndCounter.lock.newCondition();
    }
}

Тестовое задание:

public class DynamicKeyLockTest
{
    @Test
    public void testDifferentKeysDontLock() throws InterruptedException
    {
        DynamicKeyLock<Object> lock = new DynamicKeyLock<>(new Object());
        lock.lock();
        AtomicBoolean anotherThreadWasExecuted = new AtomicBoolean(false);
        try
        {
            new Thread(() ->
            {
                DynamicKeyLock<Object> anotherLock = new DynamicKeyLock<>(new Object());
                anotherLock.lock();
                try
                {
                    anotherThreadWasExecuted.set(true);
                }
                finally
                {
                    anotherLock.unlock();
                }
            }).start();
            Thread.sleep(100);
        }
        finally
        {
            Assert.assertTrue(anotherThreadWasExecuted.get());
            lock.unlock();
        }
    }

    @Test
    public void testSameKeysLock() throws InterruptedException
    {
        Object key = new Object();
        DynamicKeyLock<Object> lock = new DynamicKeyLock<>(key);
        lock.lock();
        AtomicBoolean anotherThreadWasExecuted = new AtomicBoolean(false);
        try
        {
            new Thread(() ->
            {
                DynamicKeyLock<Object> anotherLock = new DynamicKeyLock<>(key);
                anotherLock.lock();
                try
                {
                    anotherThreadWasExecuted.set(true);
                }
                finally
                {
                    anotherLock.unlock();
                }
            }).start();
            Thread.sleep(100);
        }
        finally
        {
            Assert.assertFalse(anotherThreadWasExecuted.get());
            lock.unlock();
        }
    }
}

Вы можете безопасно использовать String.intern для синхронизации, если можете разумно гарантировать, что строковое значение уникально в вашей системе. UUIDS - хороший способ подойти к этому. Вы можете связать UUID с вашим фактическим строковым ключом либо через кеш, либо через карту, либо, возможно, даже сохранить uuid как поле в вашем объекте сущности.

    @Service   
    public class MySyncService{

      public Map<String, String> lockMap=new HashMap<String, String>();

      public void syncMethod(String email) {

        String lock = lockMap.get(email);
        if (lock==null) {
            lock = UUID.randomUUID().toString();
            lockMap.put(email, lock);
        }   

        synchronized(lock.intern()) {
                //do your sync code here
        }
    }

В вашем случае вы можете использовать что-то вроде этого (это не приводит к утечке памяти):

private Synchronizer<String> synchronizer = new Synchronizer();

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) {
    String key = "Data-" + email;

    return synchronizer.synchronizeOn(key, () -> {

        SomeData[] data = (SomeData[]) StaticCache.get(key);
        if (data == null) {
            data = service.getSomeDataForEmail(email);
            StaticCache.set(key, data, CACHE_TIME);
        } else {
          logger.debug("getSomeDataForEmail: using cached object");
        }
        return data;

    });
}

чтобы использовать его, вы просто добавляете зависимость:

compile 'com.github.matejtymes:javafixes:1.3.0'

Последнее обновление 2019,

Если вы ищете новые способы реализации синхронизации в JAVA, этот ответ для вас.

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

Как синхронизировать блоки по значению объекта в Java.

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

Начиная с Java 8, вы можете использовать computeIfAbsent или ConcurrentHashMap для создания блокировок по имени. Для этого требуется всего несколько строк кода:

final ConcurrentHashMap<String, ReentrantLock> locks = new ConcurrentHashMap<>();
final ReentrantLock lock = locks.computeIfAbsent(lockName, (name) -> new ReentrantLock());

lock.lock();
try {
    // Do something.
} finally {
    lock.unlock();
}

Если именованная блокировка не существует, она будет создана computeIfAbsent атомарно.

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

igor.zh 21.02.2021 04:19

Вы должны быть очень осторожны при использовании короткоживущих объектов с синхронизацией. К каждому объекту Java прикреплен монитор, и по умолчанию этот монитор спущен; однако, если 2 потока конкурируют за получение монитора, монитор раздувается. Если объект будет долгоживущим, это не проблема. Однако, если объект недолговечен, очистка этого раздутого монитора может серьезно сказаться на времени сборки мусора (что приводит к увеличению задержек и снижению пропускной способности). И может быть даже сложно определить время сборки мусора, поскольку оно не всегда указывается.

Если вы действительно хотите синхронизировать, вы можете использовать java.util.concurrent.Lock. Или воспользуйтесь созданным вручную полосатым замком и используйте хэш строки в качестве индекса для этого полосатого замка. Этот полосатый замок вы держите, чтобы не получить проблем с сборщиком мусора.

Так что-то вроде этого:

static final Object[] locks = newLockArray();

Object lock = locks[hashToIndex(key.hashcode(),locks.length];
synchronized(lock){
       ....
}

int hashToIndex(int hash, int length) {
    if (hash == Integer.MIN_VALUE return 0;
    return abs(hash) % length;
}

Другие вопросы по теме