Java: синхронизация на примитивах? - PullRequest
14 голосов
/ 17 февраля 2010

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

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

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

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

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

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

Ответы [ 11 ]

15 голосов
/ 17 февраля 2010

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

8 голосов
/ 17 февраля 2010

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

6 голосов
/ 17 февраля 2010

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

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 голосов
/ 17 февраля 2010

Для начала:

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

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

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

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

Также есть кое-что об этом и пример StripedMap в Параллелизм Java на практике раздел 11.4.3.

4 голосов
/ 17 февраля 2010

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

public void doWork(long id) {
    LockObject lock = lockManager.GetMonitor(id);
    try {
        synchronized(lock) {
            // ...
        }
    } finally {
        lock.Release();
    }
}
0 голосов
/ 21 июня 2019

Возможно, я опаздываю к игре, но это решение не приводит к потере памяти, и вам не нужно забывать делать какие-либо блокировки релизов:

Synchronizer<AccountId> synchronizer = new Synchronizer();

...

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

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

...

// second thread - is blocked while first thread runs (as it uses the same "lock" for accountId 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'
0 голосов
/ 17 февраля 2010

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

Получив канонизирующую карту, вы напишите свой защищенный кодом код:

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

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

0 голосов
/ 17 февраля 2010

Вы можете создать список или набор активных идентификаторов и использовать 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();
}
}

Решенные проблемы:

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

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

  • while / notifyAll может привести к некоторой потере производительности при большом количестве потоков
  • Не реентерабельный
0 голосов
/ 17 февраля 2010

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

0 голосов
/ 17 февраля 2010

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

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

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

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