Вопрос

У меня есть веб-приложение, в котором я нахожусь в процессе тестирования загрузки / производительности, в частности, функции, в которой мы ожидаем, что несколько сотен пользователей будут заходить на одну и ту же страницу и нажимать "Обновить" примерно каждые 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 по истечении срока действия я подумал, что мне нужно синхронизировать операцию получения / установки кэша, и казалось, что использование ключа кэша было бы хорошим кандидатом для объекта для синхронизации (таким образом, вызовы этого метода для электронной почты b@b.com не будет заблокирован вызовами метода a@a.com).

Я обновил метод, чтобы он выглядел следующим образом:

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;
}

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

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

(вывод журнала - 'threadname', 'имя регистратора', 'сообщение')
http-80-Processor253 jsp.view-страница - getSomeDataForEmail:собираюсь войти в блок синхронизации
http-80-Processor253 jsp.view-страница - getSomeDataForEmail:внутренний блок синхронизации
http-80-Кэш процессора 253.staticCache - получить:срок действия объекта по клавише [SomeData-test@test.com] истек
http-80-Кэш процессора 253.staticCache - получить:ключ [SomeData-test@test.com] возвращает значение [null]
http-80-Processor263 jsp.просмотр-страницы - getSomeDataForEmail:собираюсь войти в блок синхронизации
http-80-Processor263 jsp.просмотр-страницы - getSomeDataForEmail:внутренний блок синхронизации
кэш http-80-Processor263.staticCache - получить:срок действия объекта по клавише [SomeData-test@test.com] истек
кэш http-80-Processor263.staticCache - получить:ключ [SomeData-test@test.com] возвращает значение [null]
http-80-Processor131 jsp.view-страница - getSomeDataForEmail:собираюсь войти в блок синхронизации
http-80-Processor131 jsp.view-страница - getSomeDataForEmail:внутренний блок синхронизации
http-80-Кэш процессора 131.staticCache - получить:срок действия объекта по клавише [SomeData-test@test.com] истек
http-80-Кэш процессора 131.staticCache - получить:ключ [SomeData-test@test.com] возвращает значение [null]
http-80-Processor104 jsp.просмотр-страницы - getSomeDataForEmail:внутренний блок синхронизации
кэш http-80-Processor104.staticCache - получить:срок действия объекта по клавише [SomeData-test@test.com] истек
кэш http-80-Processor104.staticCache - получить:ключ [SomeData-test@test.com] возвращает значение [null]
http-80-Processor252 jsp.view-страница - getSomeDataForEmail:собираюсь войти в блок синхронизации
http-80-Processor283 jsp.просмотр-страницы - getSomeDataForEmail:собираюсь войти в блок синхронизации
http-80-Processor2 jsp.view-страница - getSomeDataForEmail:собираюсь войти в блок синхронизации
http-80-Processor2 jsp.view-страница - getSomeDataForEmail:внутренний блок синхронизации

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

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

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

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

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

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

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

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

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

Это было полезно?

Решение

Не включая полностью свой мозг, при быстром просмотре того, что вы говорите, создается впечатление, что вам нужно интернировать () свои строки:

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 и медленной операции для выполнения.Перенесение всего этого в метод в кэше может быть неплохой идеей.

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

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

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

Например.:

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

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

Также:

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

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

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

Я видел два решения этой проблемы:

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

Вместо электронного письма создайте объект, содержащий электронное письмо (скажем, объект User), который содержит значение email в качестве переменной.Если у вас уже есть другой объект, представляющий человека (скажем, вы уже извлекли что-то из базы данных на основе его электронной почты), вы могли бы использовать это.Реализуя метод equals и метод hashcode, вы можете убедиться, что 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.Используя ЛРУМапs из Apache Commons сделал бы это.

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

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

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

Краткие сведения

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

Вы можете использовать утилиты параллелизма 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.

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

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

Что касается комментария о том, что String.intern() является источником утечек памяти, то на самом деле это неправда.Интернированные строки являются собранный мусор просто может занять больше времени, потому что на некоторых JVM (SUN) они хранятся в пространстве Perm, к которому прикасаются только полные GC.

Вот безопасное короткое решение 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;
}

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

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

Таким образом, он может быть более безопасным и эффективным в использовании тайник с истекающим сроком годности Гуавы:

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 является потокобезопасным и не будет страдать от одновременного чтения и записи для разных ключей.

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

Призыв:

   final String key = "Data-" + email;

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

Это еще раз объясняет вашу правку.Когда у вас есть статическая строка, тогда это будет работать.

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

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

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

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() будет зависать текущий поток на неопределенный срок.

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

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

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

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

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 / put, должна быть, по крайней мере, синхронизация операций типа get и getIfAbsentPut для безопасного доступа к кэшу.Областью синхронизации будет сам кэш.

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

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

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

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

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

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

final String key = "Data-" + email;

Есть ли в кэше другие вещи / типы объектов, которые используют адрес электронной почты, для которого вам нужны дополнительные "Данные" в начале ключа?

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

final String key = email;

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

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

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
        }
    }
}

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

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;
    });
}

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

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);
            }
        }
    }
}

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

Смотрите реализацию для 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 реализует блокировку { закрытый конечный статический ConcurrentHashMap locksMap = новый 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();
        }
    }
}

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

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'

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

    @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
        }
    }
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top