Как определить, заблокирован ли объект (синхронизирован), чтобы не блокировать в Java?

81

У меня есть процесс A, который содержит таблицу в памяти с набором записей (recordA, recordB и т. Д.)

Теперь этот процесс может запускать множество потоков, которые влияют на записи, и иногда у нас может быть 2 потока, пытающихся получить доступ к одной и той же записи - эту ситуацию необходимо отклонить. В частности, если запись ЗАБЛОКИРОВАНА одним потоком, я хочу, чтобы другой поток прервался (я не хочу БЛОКИРОВАТЬ или ЖДАТЬ).

Сейчас я делаю что-то вроде этого:

synchronized(record)
{
performOperation(record);
}

Но это вызывает у меня проблемы ... потому что, пока Process1 выполняет операцию, если Process2 входит в него, он блокирует / ожидает синхронизированного оператора, а когда Process1 завершается, он выполняет операцию. Вместо этого я хочу что-то вроде этого:

if (record is locked)
   return;

synchronized(record)
{
performOperation(record);
}

Есть какие-нибудь подсказки о том, как это можно сделать? Любая помощь приветствуется. Благодаря,

Шайтан00
источник

Ответы:

87

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

Брайан прав, указывая на него Lock, но я думаю, что вам действительно нужен его tryLockметод:

Lock lock = new ReentrantLock();
......
if (lock.tryLock())
{
    // Got the lock
    try
    {
        // Process record
    }
    finally
    {
        // Make sure to unlock so that we don't cause a deadlock
        lock.unlock();
    }
}
else
{
    // Someone else had the lock, abort
}

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

(Мне жаль, что Java API - насколько мне известно - не обеспечивает той же функциональности для «встроенной» блокировки, как Monitorкласс в .NET. Опять же, существует множество другие вещи, которые мне не нравятся на обеих платформах, когда дело доходит до потоковой передачи - например, каждый объект, потенциально имеющий монитор!)

Джон Скит
источник
Да. Неплохо подмечено. Я воспринял пример кода буквально, тогда как приведенный выше - определенно более надежная реализация
Брайан Агнью,
5
Но как использовать блокировку для каждой записи? В настоящее время записи хранятся в HashTable записей ... так мне нужна соответствующая Hashtable of Locks? Я пытаюсь обеспечить максимально возможный параллелизм, поэтому, если процесс хочет получить доступ к recordC, это должно быть нормально (если заблокирована только записьB) - я использую глобальную LOCK, тогда это по сути то же самое, что и блокировка всей хэш-таблицы. ... в этом есть смысл?
Shaitan00,
@ Shaitan00: Самый простой способ - заблокировать запись. По сути, вы хотите, чтобы с каждой записью была связана одна блокировка - поэтому поместите ее в объект.
Джон Скит,
Очевидно :) Предполагаю, что мне не нужно управлять разблокировкой? Это означает, что когда он выходит из {} .tryLock (), он будет автоматически и немедленно разблокирован правильно?
Shaitan00,
1
Вам все же нужно управлять разблокировкой. См. Руководство, на которое я ссылался в своем ответе, и метод unlock () в блоке finally {}
Брайан Агнью,
24

Взгляните на объекты Lock, представленные в пакетах параллелизма Java 5.

например

Lock lock = new ReentrantLock()
if (lock.tryLock()) {
   try {
      // do stuff using the lock...
   }
   finally {
      lock.unlock();
   }
}
   ...

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

РЕДАКТИРОВАТЬ: Как заметил Джон, isLocked()метод сообщает вам в этот момент , и после этого эта информация устарела. Метод tryLock () обеспечит более надежную работу (обратите внимание, что вы также можете использовать его с таймаутом)

РЕДАКТИРОВАТЬ № 2: Теперь tryLock()/unlock()для ясности включен пример .

Брайан Агнью
источник
11

Я нашел это, мы можем использовать, Thread.holdsLock(Object obj)чтобы проверить, заблокирован ли объект:

Возвращает, trueесли и только если текущий поток удерживает блокировку монитора для указанного объекта.

Обратите внимание, что Thread.holdsLock()возвращается, falseесли блокировка удерживается чем-то, а вызывающий поток не является потоком, который удерживает блокировку.

Рахул Кульшрешта
источник
8

Хотя описанный выше подход с использованием объекта Lock - лучший способ сделать это, если вам нужно иметь возможность проверять блокировку с помощью монитора, это можно сделать. Тем не менее, он поставляется с предупреждением о работоспособности, поскольку этот метод не переносится на виртуальные машины Java, отличные от Oracle, и может сломаться в будущих версиях виртуальных машин, поскольку это не поддерживаемый общедоступный API.

Вот как это сделать:

private static sun.misc.Unsafe getUnsafe() {
    try {
        Field field = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        return (Unsafe) field.get(null);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

public void doSomething() {
  Object record = new Object();
  sun.misc.Unsafe unsafe = getUnsafe(); 
  if (unsafe.tryMonitorEnter(record)) {
    try {
      // record is locked - perform operations on it
    } finally {
      unsafe.monitorExit(record);
    }
  } else {
      // could not lock record
  }
}

Я бы посоветовал использовать этот подход только в том случае, если вы не можете реорганизовать свой код для использования для этого объектов java.util.concurrent Lock и если вы работаете на виртуальной машине Oracle.

Крис Рэйт
источник
12
@alestanis, я не согласен. Сегодня я читаю эти ответы и рад всем ответам / комментариям, независимо от того, когда они были даны.
Том
@Tom Эти ответы отмечены системой SO, поэтому я оставил сообщение. Вы увидите, когда начнете рецензировать :)
алестанис
2
@alestanis, я думаю, что это прискорбно - я часто вижу старые вопросы, на которые есть принятые ответы, но есть и новые, лучшие ответы либо из-за технических изменений, либо из-за того, что принятый ответ на самом деле не был полностью правильным.
Том
1
Помимо того, что эта ветка старая и имеет хороший ответ, этот ответ здесь не очень хорош. Если вы действительно хотите заблокировать с помощью монитора, вы можете использовать Thread.holdsLock (объект).
Тобиас
3

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

Я буду использовать CopyOnWriteArrayList в качестве примера, потому что для иллюстрации он менее «волшебный». CopyOnWriteArraySet - более подходящая структура. Если у вас в среднем заблокировано много-много записей одновременно, эти реализации могут сказаться на производительности. Правильно синхронизированный HashSet тоже будет работать, а блокировки будут короткими.

По сути, код использования будет выглядеть так:

CopyOnWriteArrayList<Record> lockedRecords = ....
...
if (!lockedRecords.addIfAbsent(record))
    return; // didn't get the lock, record is already locked

try {
    // Do the record stuff
}        
finally {
    lockedRecords.remove(record);
}

Он избавляет вас от необходимости управлять блокировкой для каждой записи и предоставляет единое место, если по какой-либо причине потребуется снятие всех блокировок. С другой стороны, если у вас когда-либо будет больше нескольких записей, тогда реальный HashSet с синхронизацией может работать лучше, поскольку поиск по добавлению / удалению будет O (1) вместо линейного.

Просто другой взгляд на вещи. Просто зависит от ваших фактических требований к потокам. Лично я бы использовал Collections.synchronizedSet (new HashSet ()), потому что он будет очень быстрым ... единственное значение состоит в том, что потоки могут уступать, когда в противном случае они не были бы.

PSpeed
источник
Если вы хотите иметь полный контроль, Lockя думаю , вы можете поместить это в свой собственный класс.
bvdb
1

Другой обходной путь (в случае, если у вас не было шансов с ответами, приведенными здесь), использует тайм-ауты. то есть ниже один вернет null после 1 секунды зависания:

ExecutorService executor = Executors.newSingleThreadExecutor();
        //create a callable for the thread
        Future<String> futureTask = executor.submit(new Callable<String>() {
            @Override
            public String call() throws Exception {
                return myObject.getSomething();
            }
        });

        try {
            return futureTask.get(1000, TimeUnit.MILLISECONDS);
        } catch (InterruptedException | ExecutionException | TimeoutException e) {
            //object is already locked check exception type
            return null;
        }
HRgiger
источник
-2

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

Итак, вот мое предложение по УЛУЧШЕНИЮ принятого ответа:

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

  Lock localLock = new ReentrantLock();

  private void threadSafeCall() {
    boolean isUnlocked = false;

    synchronized(localLock) {
      isUnlocked = localLock.tryLock();
    }

    if (isUnlocked) {
      try {
        rawCall();
      }
      finally {
        localLock.unlock();
      }
    } else {
      LOGGER.log(Level.INFO, "THANKS! - SAVED FROM DOUBLE CALL!");
    }
  }

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

Подробнее о моих проблемах в развитии читайте в моем блоге .

Шварц
источник
1
использование synchronized для объекта блокировки очень и очень плохо. Весь смысл блокировки заключается в том, чтобы избежать синхронизации, чтобы вы могли быстро выполнить тайм-аут или быстро вернуться, когда не можете получить блокировку.
Ajax