Синхронизация по целочисленному значению

Возможный дубликат:
Как лучше всего увеличить количество блокировок в Java?

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

Существующий код не синхронизирован и может вызвать несколько операций извлечения / сохранения:

//psuedocode
public Page getPage (Integer id){
   Page p = cache.get(id);
   if (p==null)
   {
      p=getFromDataBase(id);
      cache.store(p);
   }
}

Что я хотел бы сделать, это синхронизировать получение по идентификатору, например,

   if (p==null)
   {
       synchronized (id)
       {
        ..retrieve, store
       }
   }

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

Есть ли простой способ гарантировать, что у вас есть тот же экземпляр Integer? Например, будет ли это работать:

 syncrhonized (Integer.valueOf(id.intValue())){

Javadoc для Integer.valueOf(), по-видимому, подразумевает, что вы, вероятно, получите тот же экземпляр, но это не выглядит гарантией:

Возвращает экземпляр Integer, представляющий указанное значение типа int. Если новый экземпляр Integer не требуется, этот метод обычно следует использовать в предпочтении перед конструктором Integer(int), поскольку этот метод, вероятно, даст значительно лучшую производительность пространства и времени за счет кэширования часто запрашиваемых значений.

Итак, есть ли какие-либо предложения о том, как получить экземпляр Integer, который гарантированно будет таким же, кроме более сложных решений, таких как сохранение объектов WeakHashMap из Lock, привязанных к int? (ничего плохого в этом нет, просто кажется, что должна быть очевидная однострочная строка, чем я пропускаю).

9 ответов

Решение

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

Лучше было бы создать новый объект, возможно, сохраненный в HashMap что обусловлено Integer, чтобы синхронизировать по. Что-то вроде этого:

public Page getPage(Integer id) {
  Page p = cache.get(id);
  if (p == null) {
    synchronized (getCacheSyncObject(id)) {
      p = getFromDataBase(id);
      cache.store(p);
    }
  }
}

private ConcurrentMap<Integer, Integer> locks = new ConcurrentHashMap<Integer, Integer>();

private Object getCacheSyncObject(final Integer id) {
  locks.putIfAbsent(id, id);
  return locks.get(id);
}

Чтобы объяснить этот код, он использует ConcurrentMap, что позволяет использовать putIfAbsent, Вы могли бы сделать это:

  locks.putIfAbsent(id, new Object());

но тогда вы несете (небольшую) стоимость создания объекта для каждого доступа. Чтобы избежать этого, я просто сохраняю само Integer в Map, Чего это достичь? Почему это отличается от использования самого Integer?

Когда вы делаете get() из Mapключи сравниваются с equals() (или, по крайней мере, используемый метод является эквивалентом использования equals()). Два разных экземпляра Integer одного и того же значения будут равны друг другу. Таким образом, вы можете передать любое количество различных экземпляров Integernew Integer(5)"в качестве параметра getCacheSyncObject и вы всегда получите обратно только самый первый экземпляр, который был передан, который содержал это значение.

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

  locks.putIfAbsent(id, new Object());

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

Используйте поточно-ориентированную карту, такую ​​как ConcurrentHashMap, Это позволит вам безопасно управлять картой, но использовать другую блокировку для выполнения реальных вычислений. Таким образом, вы можете выполнять несколько вычислений одновременно с одной картой.

использование ConcurrentMap.putIfAbsent, но вместо размещения фактического значения используйте Future вместо вычислительно легкой конструкции. Возможно FutureTask реализация. Запустите вычисление, а затем get результат, который будет блокировать потокобезопасность, пока не будет сделано.

Integer.valueOf() возвращает только кэшированные экземпляры для ограниченного диапазона. Вы не указали свой диапазон, но в целом это не сработает.

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

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

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

Использование синхронизации по Integer звучит действительно неправильно по дизайну.

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

// this contains only those IDs that are currently locked, that is, this
// will contain only very few IDs most of the time
Set<Integer> activeIds = ...

Object retrieve(Integer id) {
    // acquire "lock" on item #id
    synchronized(activeIds) {
        while(activeIds.contains(id)) {
            try { 
                activeIds.wait();   
            } catch(InterruptedExcption e){...}
        }
        activeIds.add(id);
    }
    try {

        // do the retrieve here...

        return value;

    } finally {
        // release lock on item #id
        synchronized(activeIds) { 
            activeIds.remove(id); 
            activeIds.notifyAll(); 
        }   
    }   
}

То же самое относится и к магазину.

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

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

  • Подход Гетца и его сотрудников по сохранению кэша FutureTasks работает довольно хорошо в ситуациях, подобных этой, когда вы все равно "кэшируете что-то", так что не стесняйтесь создавать карту объектов FutureTask (и, если вы не возражаете, рост карты, по крайней мере, легко подрезать его одновременно)
  • В качестве общего ответа на вопрос "как заблокировать идентификатор" подход, описанный Антонио, имеет то преимущество, что он очевиден, когда карта блокировок добавляется / удаляется из.

Возможно, вам придется остерегаться потенциальной проблемы с реализацией Antonio, а именно того, что notifyAll() будет вызывать потоки, ожидающие всех идентификаторов, когда один из них станет доступным, что может не очень хорошо масштабироваться в условиях высокой конкуренции. В принципе, я думаю, что вы можете исправить это, имея объект Condition для каждого заблокированного в настоящее время идентификатора, который вы ожидаете / сигнализируете. Конечно, если на практике редко ожидают более одного ID в любой момент времени, то это не проблема.

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

Стив,

Ваш предложенный код имеет кучу проблем с синхронизацией. (Антонио тоже)

Подвести итоги:

  1. Вам нужно кэшировать дорогой объект.
  2. Необходимо убедиться, что пока один поток выполняет поиск, другой поток также не пытается извлечь тот же объект.
  3. Что для n-потоков все попытки получить объект только 1 объект когда-либо извлекаются и возвращаются.
  4. Это для потоков, запрашивающих разные объекты, чтобы они не конкурировали друг с другом.

псевдокод, чтобы сделать это (используя ConcurrentHashMap в качестве кэша):

ConcurrentMap<Integer, java.util.concurrent.Future<Page>> cache = new ConcurrentHashMap<Integer, java.util.concurrent.Future<Page>>;

public Page getPage(Integer id) {
    Future<Page> myFuture = new Future<Page>();
    cache.putIfAbsent(id, myFuture);
    Future<Page> actualFuture = cache.get(id);
    if ( actualFuture == myFuture ) {
        // I am the first w00t!
        Page page = getFromDataBase(id);
        myFuture.set(page);
    }
    return actualFuture.get();
}

Замечания:

  1. java.util.concurrent.Future - это интерфейс
  2. java.util.concurrent.Future на самом деле не имеет set(), но посмотрите на существующие классы, которые реализуют Future, чтобы понять, как реализовать свое собственное Future (или использовать FutureTask).
  3. Перенос фактического поиска в рабочий поток почти наверняка будет хорошей идеей.

Как насчет ConcurrentHashMap с объектами Integer в качестве ключей?

См. Раздел 5.6 " Практический параллелизм Java": "Создание эффективного масштабируемого кэша результатов". Это касается именно той проблемы, которую вы пытаетесь решить. В частности, проверьте шаблон памятника.

http://www.cs.umd.edu/class/fall2008/cmsc433/jcipMed.jpg

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