Java: Синхронизация на примитивах?

В нашей системе у нас есть метод, который будет выполнять некоторую работу при вызове с определенным ID:

public void doWork(long id) { /* ... */ }

Теперь эта работа может выполняться одновременно для разных идентификаторов, но если метод вызывается с тем же идентификатором по 2 потокам, один поток должен блокироваться до его завершения.

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

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

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

Ответы

Ответ 1

Я изобрел такую ​​вещь для себя некоторое время назад. Я называю это блокировкой класса эквивалентности, то есть он блокирует все вещи, которые равны данной вещи. Вы можете получить из моего github и использовать его под лицензией Apache 2, если хотите, или просто прочитать его и забыть!

Ответ 2

Вы можете попробовать что-то с помощью ReentrantLock, так что у вас есть Map<Long,Lock>. Теперь после lock.release() Вы можете проверить lock.hasQueuedThreads(). Если это возвращает false, вы можете удалить его с карты.

Ответ 3

Вы можете попробовать следующий маленький "взломать"

String str = UNIQUE_METHOD_PREFIX + Long.toString(id);
synchornized(str.intern()) { .. }

на 100% гарантированный возврат одного и того же экземпляра.

UNIQUE_METHOD_PREFIX, может быть жестко запрограммированной константой или может быть получена с использованием:

StackTraceElement ste = Thread.currentThread().getStackTrace()[0];
String uniquePrefix = ste.getDeclaringClass() + ":" +ste.getMethodName();

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

Ответ 4

Я бы сказал, что у вас уже довольно много решений. Сделайте LockManager, который лениво и ориентирован на учет, чтобы управлять этими замками для вас. Затем используйте его в doWork:

public void doWork(long id) {
    LockObject lock = lockManager.GetMonitor(id);
    try {
        synchronized(lock) {
            // ...
        }
    } finally {
        lock.Release();
    }
}

Ответ 5

Для начала:

  • Вы не можете заблокировать примитив и
  • Не блокируйте Long, если не будете осторожны, как вы их создаете. Длинные значения, создаваемые autoboxing или Long.valueOf() в определенном диапазоне, гарантируются одинаковыми для JVM, что означает, что другие потоки могут блокироваться на одном и том же длинном объекте и давать вам перекрестный разговор. Это может быть тонкая ошибка concurrency (аналогичная блокировке на интернированных строках).

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

ConcurrentHashMap можно использовать для достижения этого, поскольку CHM состоит из сегментов (суб-карт) и есть один замок за каждый сегмент. Это дает вам concurrency равное количеству сегментов (по умолчанию 16, но настраивается).

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

В этом также есть материал и пример StripedMap в Java concurrency в разделе Практика в разделе 11.4.3.

Ответ 6

Не было бы достаточно использовать SynchronizedHashMap или Collections.synchronizedMap(Map m) из пакета java.util.concurrent вместо простой HashMap, где запросы на извлечение и вставку не синхронизированы?

что-то вроде:

Map<Long,Object> myMap = new HashMap<Long,Object>();
Map<Long,Object> mySyncedMap=Collections.synchronizedMap(myMap);

Ответ 7

Преждевременная оптимизация - это корень зла

Попробуйте с помощью (синхронизированной) карты.

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

Ответ 8

Вы можете создать список или набор активных идентификаторов и использовать wait и notify:

List<Long> working;
public void doWork(long id) {
synchronized(working)
{
   while(working.contains(id))
   {
      working.wait();
   }
   working.add(id)//lock
}
//do something
synchronized(working)
{
    working.remove(id);//unlock
    working.notifyAll();
}
}

Проблемы решены:

  • Только потоки с одинаковым ожиданием id, все остальные являются параллельными
  • Нет памяти Утечка, так как блокировки (Long) будут удалены при разблокировке
  • Работает с autoboxing

Проблемы там:

  • while/notifyAll может вызвать потерю производительности при большом количестве потоков
  • Не реентерант

Ответ 9

Здесь я бы использовал канонизирующее отображение, которое берет ваш вход long и возвращает канонический long объект, который затем можно использовать для синхронизации. Я написал о канонификации карт здесь; просто замените String на long (и чтобы сделать вашу жизнь проще, пусть она принимает long в качестве параметра).

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

Long lockObject = canonMap.get(id);
synchronized (lockObject)
{
    // stuff
}

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

Ответ 10

Я мог бы опоздать на игру, но вы могли бы сделать что-то вроде этого:

Synchronizer<AccountId> synchronizer = new Synchronizer();

...

// first thread - acquires "lock" for account accAAA

synchronizer.synchronizeOn(accountId("accAAA"), () -> {
    long balance = loadBalance("accAAA")
    if (balance > 10_000) {
        decrementBalance("accAAA", 10_000)
    }
})

...

// second thread - is blocked until first thread finishes as it uses the same "lock" for account accAAA

synchronizer.synchronizeOn(accountId("accAAA"), () -> {
    long balance = loadBalance("accAAA")
    if (balance > 2_000) {
        decrementBalance("accAAA", 2_000)
    }
})

...

// third thread - won't be blocked by previous threads (as it is for a different accountId)

synchronizer.synchronizeOn(accountId("accXYZ"), () -> {
    long balance = loadBalance("accXYZ")
    if (balance > 3_500) {
        decrementBalance("accXYZ", 3_500)
    }
})

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

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

Ответ 11

Я предлагаю вам использовать утилиты из java.util.concurrent, особенно класса AtomicLong. См. связанный javadoc