Блокировка потоков Java на конкретном объекте - PullRequest
3 голосов
/ 07 июля 2011

У меня есть веб-приложение, и я использую базу данных Oracle, и у меня есть метод, в основном такой:

public static void saveSomethingImportantToDataBase(Object theObjectIwantToSave) {
      if (!methodThatChecksThatObjectAlreadyExists) {
         storemyObject() //pseudo code
     }
     // Have to do a lot other saving stuff, because it either saves everything or nothing
     commit() // pseudo code to actually commit all my changes to the database.
}

В настоящее время нет никакой синхронизации, так что n потоков могут, конечно, свободно обращаться к этому методу, проблема возникает, когда 2 потока входят в этот метод и проверяют, и, конечно, ничего еще нет, и тогда они оба могут зафиксировать транзакция, создание дубликата объекта.

Я не хочу решать эту проблему с помощью уникального ключевого идентификатора в моей Базе данных, потому что я не думаю, что мне следует ловить это SQLException.

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

Мой опыт работы с блокировками и потоками ограничен, но моя идея в основном заключается в том, чтобы заблокировать этот код на объекте, который он получает. Я не знаю, скажем, например, что я получил объект Integer, и я блокирую свое Integer со значением 1, что будет препятствовать входу только потоков с другим Integer со значением 1, и все другие потоки с value != 1 могут войти свободно ?, это так работает?

Кроме того, если это так, как сравнивается объект блокировки? как определяется, что они на самом деле являются одним и тем же объектом? Хорошая статья по этому вопросу также будет оценена.

Как бы вы решили это?

Ответы [ 10 ]

5 голосов
/ 07 июля 2011

Ваша идея хорошая.Это упрощенная / наивная версия, но она вряд ли будет работать:

public static void saveSomethingImportantToDataBase(Object theObjectIwantToSave) {
    synchronized (theObjectIwantToSave) {
        if (!methodThatChecksThatObjectAlreadyExists) {
            storemyObject() //pseudo code
        }
        // Have to do a lot other saving stuff, because it either saves everything or nothing
        commit() // pseudo code to actually commit all my changes to the database.
    }
}

Этот код использует сам объект в качестве блокировки.Но это должен быть такой же объект (т.е. objectInThreadA == objectInThreadB), если он должен работать.Если два потока работают над объектом, который является копией друг друга - то есть, например, с одним и тем же "идентификатором", то вам нужно будет либо синхронизировать весь метод:

    public static synchronized void saveSomethingImportantToDataBase(Object theObjectIwantToSave) ...

, что, конечно, значительно сократит параллелизм (пропускная способность будет уменьшаться до одного потока за раз, используя метод, которого следует избегать).

Или найдите способ получить такую ​​же блокировку объект, основанный на объекте сохранения, подобный этому подходу:

private static final ConcurrentHashMap<Object, Object> LOCKS = new ConcurrentHashMap<Object, Object>();
public static void saveSomethingImportantToDataBase(Object theObjectIwantToSave) {
    synchronized (LOCKS.putIfAbsent(theObjectIwantToSave.getId(), new Object())) {
        ....    
    }
    LOCKS.remove(theObjectIwantToSave.getId()); // Clean up lock object to stop memory leak
}

Эта последняя версия является рекомендованной: она гарантирует, что два объекта сохранения, которые имеют один и тот же идентификатор, заблокированы одним и тем же объектом блокировки.метод ConcurrentHashMap.putIfAbsent() является потокобезопасным, поэтому «это будет работать», и для правильной работы требуется только, чтобы objectInThreadA.getId().equals(objectInThreadB.getId()).Кроме того, тип данных getId () может быть любым, включая примитивы (например, int) из-за автобоксирования в java .

Если вы переопределяете equals() и hashcode() для вашего объекта, тогда вы можете использовать сам объект вместо object.getId(), и это было бы улучшением (спасибо @TheCapn за указание на это)

Это решение будет работать только в одной JVM.Если ваши серверы сгруппированы, то совершенно другая игра с мячом и механизм блокировки java вам не помогут.Вам придется использовать решение кластерной блокировки, которое выходит за рамки этого ответа.

3 голосов
/ 30 марта 2015

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

import java.util.HashMap;
import java.util.concurrent.atomic.AtomicInteger;

// it is no advantage of using ConcurrentHashMap, since we synchronize access to it
// (we need to in order to "get" the lock and increment/decrement it safely)
// AtomicInteger is just a mutable int value holder
// we don't actually need it to be atomic
static final HashMap<Object, AtomicInteger> locks = new HashMap<Integer, AtomicInteger>();

public static void saveSomethingImportantToDataBase(Object objectToSave) {
    AtomicInteger lock;
    synchronized (locks) {
        lock = locks.get(objectToSave.getId());
        if (lock == null) {
            lock = new AtomicInteger(1);
            locks.put(objectToSave.getId(), lock);
        }
        else 
          lock.incrementAndGet();
    }
    try {
        synchronized (lock) {
            // do synchronized work here (synchronized by objectToSave's id)
        }
    } finally {
        synchronized (locks) {
            lock.decrementAndGet();
            if (lock.get() == 0)  
              locks.remove(id);
        }
    }
}

Вы можете разделить их на вспомогательные методы «получить блокировку объекта» и «снять блокировку» или что-либо еще, чтобы очистить код.Этот способ кажется немного более хитрым, чем мой другой ответ .

2 голосов
/ 06 января 2015

Ответ Богемиана, похоже, имеет проблемы с состоянием гонки, если один поток находится в синхронизированном разделе, в то время как другой поток удаляет объект синхронизации с карты и т. Д. Так что здесь есть альтернатива, которая использует WeakRef.

// there is no synchronized weak hash map, apparently
// and Collections.synchronizedMap has no putIfAbsent method, so we use synchronized(locks) down below

WeakHashMap<Integer, Integer> locks = new WeakHashMap<>(); 

public void saveSomethingImportantToDataBase(DatabaseObject objectToSave) {
  Integer lock;
  synchronized (locks) {
    lock = locks.get(objectToSave.getId());
    if (lock == null) {
      lock = new Integer(objectToSave.getId());
      locks.put(lock, lock);
    }
  }
  synchronized (lock) {
    // synchronized work here (synchronized by objectToSave's id)
  }
  // no releasing needed, weakref does that for us, we're done!
}

И более конкретный пример использования вышеуказанной системы стилей:

static WeakHashMap<Integer, Integer> locks = new WeakHashMap<>(); 

static Object getSyncObjectForId(int id) {
  synchronized (locks) {
    Integer lock = locks.get(id);
    if (lock == null) {
      lock = new Integer(id);
      locks.put(lock, lock);
    }
    return lock;
  }
}

Затем используйте ее в другом месте, например:

...
  synchronized (getSyncObjectForId(id)) {
    // synchronized work here
  }
...

Причинав основном это работает так: если два объекта с совпадающими ключами входят в критический блок, второй извлечет блокировку, которую первый уже использует (или тот, который оставлен позади и еще не был собран GC).Однако, если он не используется, оба оставят метод и удалили свои ссылки на объект блокировки, поэтому он будет безопасно собран.

Если у вас ограниченный «известный размер» точек синхронизации, которые вы хотите использовать(тот, который не должен уменьшаться в размере в конце концов), вы, вероятно, могли бы избежать использования HashMap и использовать вместо этого ConcurrentHashMap с его методом putIfAbsent, который может быть легче понять.

1 голос
/ 04 июня 2016

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

  1. Создать таблицу с объектами блокировки. Чем больше таблица, тем меньше шансов на чрезмерную синхронизацию.
  2. Примените некоторую функцию хеширования к вашему идентификатору для вычисления индекса таблицы. Если ваш идентификатор числовой, вы можете просто использовать функцию остаток (по модулю), если это строка, используйте hashCode () и остаток.
  3. Получить блокировку из таблицы и синхронизироваться на ней.

Класс IdLock:

public class IdLock {

private Object[] locks = new Object[10000];

public IdLock() {
  for (int i = 0; i < locks.length; i++) {
    locks[i] = new Object();
  }
}

public Object getLock(int id) {
  int index = id % locks.length;
  return locks[index];
}

}

и его использование:

private idLock = new IdLock();

public void saveSomethingImportantToDataBase(Object theObjectIwantToSave) {
  synchronized (idLock.getLock(theObjectIwantToSave.getId())) {
    // synchronized work here
  }
}
1 голос
/ 08 июля 2011

Мое мнение, вы не боретесь с реальной проблемой потоков.

Вам лучше позволить СУБД автоматически назначать не конфликтующий идентификатор строки.

Если вам нужно работать с существующими идентификаторами строк, сохраните их как локальные переменные потока. Если нет необходимости в общих данных, не делитесь данными между потоками.

http://download.oracle.com/javase/6/docs/api/java/lang/ThreadLocal.html

Oracle dbms намного лучше поддерживает согласованность данных при работе сервера приложений или веб-контейнера.

"Многие системы баз данных автоматически генерируют уникальное поле ключа при вставке строки. Oracle Database предоставляет те же функциональные возможности с помощью последовательностей и триггеров. JDBC 3.0 представляет функцию поиска автоматически сгенерированных ключей, которая позволяет вам получать такие сгенерированные значения. В JDBC 3.0 следующие интерфейсы расширены для поддержки функции извлечения автоматически сгенерированных ключей .... "

http://download.oracle.com/docs/cd/B19306_01/java.102/b14355/jdbcvers.htm#CHDEGDHJ

0 голосов
/ 12 февраля 2019
private static final Set<Object> lockedObjects = new HashSet<>();

private void lockObject(Object dbObject) throws InterruptedException {
    synchronized (lockedObjects) {
        while (!lockedObjects.add(dbObject)) {
            lockedObjects.wait();
        }
    }
}

private void unlockObject(Object dbObject) {
    synchronized (lockedObjects) {
        lockedObjects.remove(dbObject);
        lockedObjects.notifyAll();
    }
}

public void saveSomethingImportantToDatabase(Object theObjectIwantToSave) throws InterruptedException {
    try {
        lockObject(theObjectIwantToSave);

        if (!methodThatChecksThatObjectAlreadyExists(theObjectIwantToSave)) {
            storeMyObject(theObjectIwantToSave);
        }
        commit();
    } finally {
        unlockObject(theObjectIwantToSave);
    }
}
  • Вы должны корректно переопределить методы 'равно' и 'hashCode' для классов ваших объектов.Если у вас есть уникальный id (строка или число) внутри вашего объекта, тогда вы можете просто проверить этот идентификатор вместо всего объекта, и вам не нужно переопределять 'equals' и 'hashCode'.
  • try-finally - это очень важно - вы должны гарантировать разблокировку ожидающих потоков после операции, даже если ваша операция вызвала исключение.
  • Этот подход не будет работать, если ваш бэкэнд распределен по несколько серверов .
0 голосов
/ 07 июля 2011

Чтобы ответить на ваш вопрос о блокировке целого числа, краткий ответ - НЕТ - это не помешает входу потоков с другим экземпляром целого числа с таким же значением.Длинный ответ: зависит от того, как вы получите конструктор Integer - by, путем повторного использования некоторых экземпляров или с помощью valueOf (который использует некоторое кэширование).В любом случае, я бы не стал полагаться на это.

Рабочее решение, которое будет работать, - это синхронизировать метод:

public static synchronized void saveSomethingImportantToDataBase(Object theObjectIwantToSave) {
    if (!methodThatChecksThatObjectAlreadyExists) {
        storemyObject() //pseudo code
    }
    // Have to do a lot other saving stuff, because it either saves everything or nothing
    commit() // pseudo code to actually commit all my changes to the database.
}

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

0 голосов
/ 07 июля 2011

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

private static HashSet <Integer>isUsed= new HashSet <Integer>();

public synchronized static void saveSomethingImportantToDataBase(Object theObjectIwantToSave) {

      if(isUsed.contains(theObjectIwantToSave.your_integer_value) != null) {

      if (!methodThatChecksThatObjectAlreadyExists) {
         storemyObject() //pseudo code
      }
 // Have to do a lot other saving stuff, because it either saves everything or nothing
      commit() // pseudo code to actually commit all my changes to the database.
      isUsed.add(theObjectIwantToSave.your_integer_value);

  }
}
0 голосов
/ 07 июля 2011

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

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

0 голосов
/ 07 июля 2011
public static void saveSomethingImportantToDataBase(Object theObjectIwantToSave) {
  synchronized (theObjectIwantToSave) {

      if (!methodThatChecksThatObjectAlreadyExists) {
         storemyObject() //pseudo code
      }
 // Have to do a lot other saving stuff, because it either saves everything or nothing
      commit() // pseudo code to actually commit all my changes to the database.
  }
}

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

...