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

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

У меня есть webapp, который я нахожусь в середине тестирования нагрузки и производительности, особенно на функции, где мы ожидаем, что несколько сотен пользователей будут получать доступ к одной и той же странице и будут обновляться каждые 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 истек, мне показалось, что мне нужно синхронизировать операцию get/set Cache, и казалось, что использование ключа кеша будет хороший кандидат на объект для синхронизации (таким образом, вызов этого метода для электронной почты [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;
}

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

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

(выход журнала - это "имя_потока" 'имя журнала' 'сообщение')
http-80-Processor253 jsp.view-page - getSomeDataForEmail: собирается ввести блок синхронизации
http-80-Processor253 jsp.view-page - getSomeDataForEmail: внутренний блок синхронизации
http-80-Processor253 cache.StaticCache - get: объект в ключе [[email protected]] истек

http-80-Processor253 cache.StaticCache - get: key [[email protected]] возвращающее значение [null]
http-80-Processor263 jsp.view-page - getSomeDataForEmail: собирается ввести блок синхронизации
http-80-Processor263 jsp.view-page - getSomeDataForEmail: внутренний блок синхронизации
http-80-Processor263 cache.StaticCache - get: объект с ключом [[email protected]] истек

http-80-Processor263 cache.StaticCache - get: key [[email protected]] возвращающее значение [null]
http-80-Processor131 jsp.view-page - getSomeDataForEmail: собирается ввести блок синхронизации
http-80-Processor131 jsp.view-page - getSomeDataForEmail: внутренний блок синхронизации
http-80-Processor131 cache.StaticCache - get: объект в ключе [[email protected]] истек

http-80-Processor131 cache.StaticCache - get: key [[email protected]] возвращающее значение [null]
http-80-Processor104 jsp.view-page - getSomeDataForEmail: внутренний блок синхронизации
http-80-Processor104 cache.StaticCache - get: объект с ключом [[email protected]] истек

http-80-Processor104 cache.StaticCache - get: key [[email protected]] возвращающее значение [null]
http-80-Processor252 jsp.view-page - getSomeDataForEmail: собирается ввести блок синхронизации
http-80-Processor283 jsp.view-page - getSomeDataForEmail: собирается ввести блок синхронизации
http-80-Processor2 jsp.view-page - getSomeDataForEmail: собирается ввести блок синхронизации
http-80-Processor2 jsp.view-page - getSomeDataForEmail: внутренний блок синхронизации

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

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

Что я здесь делаю неправильно?

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

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

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

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

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

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

4b9b3361

Ответ 1

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

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

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

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

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

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

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

Edit

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

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

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

Ответ 2

Синхронизация на внутренней строке может быть не совсем хорошей идеей - путем ее интернирования 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
}

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

Если вы отменили кеширование через некоторое время, вы должны проверить, являются ли данные еще нулевыми после извлечения из кэша в случае блокировки!= null.

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

Ответ 3

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

также:

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

Ответ 4

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

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

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

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

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

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

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

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

Резюме

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

Ответ 5

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

 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.

Ответ 6

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

Реализация хорошего кеша не так проста, как считают некоторые люди.

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

Ответ 7

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

Ответ 8

Вызов:

   final String key = "Data-" + email;

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

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

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

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

Ответ 9

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

Ответ 10

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

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

Ответ 11

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

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

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

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

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

Ответ 12

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

Ответ 13

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

final String key = "Data-" + email;

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

Если нет, я просто сделаю это

final String key = email;

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

Ответ 14

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

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

Ответ 15

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

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

Ответ 16

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

Смотрите реализацию для 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 {private final static static 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();
        }
    }
}

Ответ 17

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

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'

Ответ 18

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

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

enter image description here

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

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

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

Ответ 19

Вы можете безопасно использовать 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
        }
    }