Есть ли способ синхронизации с использованием двух объектов блокировки в Java? - PullRequest
3 голосов
/ 02 ноября 2011

Мне интересно, есть ли способ в Java для синхронизации с использованием двух объектов блокировки. Я не имею в виду блокировку на или объекте, я имею в виду блокировку только на обоих .

например. если у меня 4 темы:

  • Поток A запрашивает блокировку, используя Object1 и Object2
  • Поток B запрашивает блокировку, используя Object1 и Object3
  • Поток C запрашивает блокировку, используя Object4 и Object2
  • Поток D запрашивает блокировку, используя Object1 и Object2

В приведенном выше сценарии поток A и поток D совместно используют блокировку, но поток B и поток C будут иметь свои собственные блокировки. Даже если они перекрываются с одним из двух объектов, одна и та же блокировка применяется только в том случае, если она перекрывает оба объекта.

Итак, у меня есть метод, вызываемый многими потоками, который собирается выполнять определенный тип активности на основе конкретной базы данных. У меня есть объекты-идентификаторы как для базы данных, так и для действия, и я могу гарантировать, что это действие будет поточно-ориентированным, если оно не совпадает с действием в той же базе данных, что и другой поток.

Мой идеальный код будет выглядеть примерно так:

public void doActivity(DatabaseIdentifier dbID, ActivityIdentifier actID) {    
    synchronized( dbID, actID ) { // <--- Not real Java
       // Do an action that can be guaranteed thread-safe per unique
       // combination of dbIT and actID, but needs to share a 
       // lock if they are both the same.
    }
}

Я мог бы создать хэш-карту объектов блокировки, которые обрабатываются как DatabaseIdentifier, так и ActivityIdentifier, но я столкнусь с одной и той же проблемой синхронизации, когда мне потребуется создать / получить доступ к этим блокировкам потокобезопасным способом.

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

У кого-нибудь есть хороший способ справиться с этим, который не требует принудительного ожидания ненужных потоков?

Спасибо!

Ответы [ 4 ]

4 голосов
/ 02 ноября 2011

каждый DatabaseIdentifier сохраняет набор блокировок на ActivityIdentifier s, которым он владеет

, чтобы вы могли позвонить

public void doActivity(DatabaseIdentifier dbID, ActivityIdentifier actID) {    
    synchronized( dbID.getLock(actID) ) { 
       // Do an action that can be guaranteed thread-safe per unique
       // combination of dbIT and actID, but needs to share a 
       // lock if they are both the same.
    }
}

, тогда вам нужен только (короткий) замокв базовой коллекции (используйте ConcurrentHashMap) в dbID

, другими словами

ConcurrentHashMap<ActivityIdentifier ,Object> locks = new...
public Object getLock(ActivityIdentifier actID){
    Object res = locks.get(actID); //avoid unnecessary allocations of Object

    if(res==null) {
        Object newLock = new Object();
        res = locks.puIfAbsent(actID,newLock );
        return res!=null?res:newLock;
    } else return res;
}

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

обновление в ответ на комментарии о EnumMap

private final EnumMap<ActivityIdentifier ,Object> locks;

/**
  initializer ensuring all values are initialized 
*/
{
    EnumMap<ActivityIdentifier ,Object> tmp = new EnumMap<ActivityIdentifier ,Object>(ActivityIdentifier.class)
    for(ActivityIdentifier e;ActivityIdentifier.values()){
        tmp.put(e,new Object());
    }
    locks = Collections.unmodifiableMap(tmp);//read-only view ensures no modifications will happen after it is initialized making this thread-safe
}


public Object getLock(ActivityIdentifier actID){
    return locks.get(actID);
}
2 голосов
/ 02 ноября 2011

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

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

Я бы предложил двухуровневую карту, примерно такую:

Map<DatabaseIdentifier, Map<ActivityIdentifier, Lock>> locks;

Используется неопределенно, таким образом:

synchronized (locks.get(databaseIdentifier).get(activityIdentifier)) {
    performSpecificActivityOnDatabase();
}

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

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

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

Map<ActivityIdentifier, Object> locksForDatabase;
synchronized (locks) {
    locksForDatabase = locks.get(databaseIdentifier);
    if (locksForDatabase == null) {
        locksForDatabase = new HashMap<ActivityIdentifier, Object>();
        locks.put(databaseIdentifier, locksForDatabase);
    }
}
Object lock;
synchronized (locksForDatabase) {
    lock = locksForDatabase.get(locksForDatabase);
    if (lock == null) {
        lock = new Object();
        locksForDatabase.put(locksForDatabase, lock);
    }
}
synchronized (lock) {
    performSpecificActivityOnDatabase();
}

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

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

ConcurrentMap<DatabaseIdentifier, Map<ActivityIdentifier, Object>> locks;

И:

Map<ActivityIdentifier, Object> newHashMap = new HashMap<ActivityIdentifier, Object>();
Map<ActivityIdentifier, Object> locksForDatabase = locks.putIfAbsent(databaseIdentifier, newHashMap);
if (locksForDatabase == null) locksForDatabase = newHashMap;
Object lock;
synchronized (locksForDatabase) {
    lock = locksForDatabase.get(locksForDatabase);
    if (lock == null) {
        lock = new Object();
        locksForDatabase.put(locksForDatabase, lock);
    }
}
synchronized (lock) {
    performSpecificActivityOnDatabase();
}

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

Тем не менее, будет создаваться устойчивый поток из HashMap экземпляров, которые будут поданы в putIfAbsentа затем выбрасывают.Вы можете избежать этого с помощью своего рода постмодернистского атомарного ремикса с двойной проверкой блокировки;замените первые три строки на:

Map<ActivityIdentifier, Object> locksForDatabase = locks.get(databaseIdentifier);
if (locksForDatabase == null) {
    Map<ActivityIdentifier, Object> newHashMap = new HashMap<ActivityIdentifier, Object>();
    locksForDatabase = locks.putIfAbsent(databaseIdentifier, newHashMap);
    if (locksForDatabase == null) locksForDatabase = newHashMap;
}

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

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

PS Меня всегда слегка раздражает, когда я вижу экземпляр объекта, который используется только как замок.Я предлагаю назвать их LockGuffins .

2 голосов
/ 02 ноября 2011

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

FlyweightAllObjectsLock lockObj = FlyweightAllObjectsLock.newInstance(dbID, actID);

Затем заблокируйте этот объект.Флайбриджная фабрика может установить блокировку чтения на карте, чтобы увидеть, есть ли там ключ, и сделать блокировку записи, только если ее нет.Это должно снизить коэффициент параллелизма.

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

0 голосов
/ 02 ноября 2011

Ваш совет по хэш-карте - это то, что я делал в прошлом. Единственное изменение, которое я бы сделал, - это использование ConcurrentHashMap для минимизации синхронизации.

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

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