Блокировка всех экземпляров класса в Java

Я внедряю параллельную банковскую систему, в которой все операции могут выполняться одновременно. Я реализовал потокобезопасный transferMoney метод, который переводит amount из Учетной записи from в to.

transferMoney реализуется следующим кодом:

public boolean transferMoney(Account from, Account to, int amount) {
        if (from.getId() == to.getId()){
            return false;
        }else if(from.getId() < to.getId()) {
            synchronized(to) {
                synchronized(from) {
                    if(from.getBalance() >= amount) {
                        from.setBalance(from.getBalance()-amount);
                        to.setBalance(to.getBalance()+amount);
                    }else {
                        return false;
                    }
                }
            }
        }else {
            synchronized(from) {
                synchronized(to) {
                    if(from.getBalance() >= amount) {
                        from.setBalance(from.getBalance()-amount);
                        to.setBalance(to.getBalance()+amount);
                    }else {
                        return false;
                    }
                }
            }
        }

        return true;
    }

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

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

public int sumAccounts(List<Account> accounts) {
    AtomicInteger sum = new AtomicInteger();

    synchronized(Account.class) {
        for (Account a : accounts) {
            sum.getAndAdd(a.getBalance());
        }
    }

    return sum.intValue();
}

Проблема

Когда я запускаю sumAccounts() одновременно с transferMoney(), я получу больше (иногда меньше) денег в банке раньше, даже если деньги не добавлялись. Насколько я понимаю, если я заблокирую все Account объекты через synchronized(Account.class), не должен ли я получить правильную сумму банка, поскольку я блокирую выполнение transferMoney()?

Что я пробовал так далеко

Я пробовал следующее:

  • синхронизация Account.class, как указано выше (не работает)
  • синхронизация конкретной учетной записи в цикле for each (но, конечно, это небезопасно для потоков, поскольку транзакции выполняются одновременно)
  • синхронизация обоих методов через объект ReentrantLock. Это работает, но сильно снижает производительность (занимает в три раза больше, чем последовательный код)
  • синхронизация обоих методов на уровне класса. Это тоже работает, но опять же занимает в три раза больше времени, чем выполнение операций последовательно.

Разве блокировка Account.class не должна предотвращать дальнейшие transferMoney() казни? Если нет, как я могу исправить эту проблему?

Изменить: код для getBalance():

public int getBalance() {
        return balance;
}

person pr0f3ss    schedule 06.04.2019    source источник
comment
Синхронизация с Account.class не включает мониторы ни в одном экземпляре Account; см. также stackoverflow.com/questions/2056243/   -  person Daniele    schedule 06.04.2019
comment
Account.getBalance() синхронизированный метод? В противном случае вы получите баланс непосредственно перед тем, как он будет вычтен из учетной записи и добавлен баланс только что добавленной учетной записи.   -  person Cratylus    schedule 06.04.2019
comment
@Cratylus Нет, это не синхронизируется, поскольку я блокирую объекты при доступе к ним в transferMoney(). Я добавил его для проверки, и он по-прежнему дает неправильный результат.   -  person pr0f3ss    schedule 06.04.2019
comment
@ pr0f3ss: Вы можете опубликовать код для getBalance()?   -  person Cratylus    schedule 07.04.2019
comment
@Cratylus просто добавил это как правку к вопросу   -  person pr0f3ss    schedule 07.04.2019


Ответы (3)


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

Использование ReentrantLock и синхронизация обоих методов на уровне класса будет вести себя так же, как Вы заявили, потому что они не позволят одновременное выполнение метода transferMoney.

образец кода:

final ReadWriteLock rwl = new ReentrantReadWriteLock();

public boolean transferMoney(Account from, Account to, int amount) {
  rwl.readLock().lock();
  try{
    .... Your current code here
  }
  finally {
       rwl.readLock().unlock();
  }
}

public int sumAccounts(List<Account> accounts) {
  rwl.writeLock().lock();
  try{
    // You dont need atomic integer here, because this can be executed by one thread at a time
    int sum = 0;
    for (Account a : accounts) {
        sum += a.getBalance();
    }
    return sum;
  }
  finally {
       rwl.writeLock().unlock();
  }
}

Также справедливый режим реентерабельных блокировок будет работать медленнее, чем несправедливые режимы. Подробности смотрите в документации.

https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/ReentrantReadWriteLock.html.

person miskender    schedule 06.04.2019
comment
Просто запустил этот код. На самом деле он возвращает неверную сумму (не знаю почему, хотя, как и раньше, я реализовал ее с помощью обычного объекта ReentrantLock, и это сработало). - person pr0f3ss; 06.04.2019
comment
@ pr0f3ss Если это дает неправильный результат, это может быть связано с тем, что действия, выполняемые потоками в transferMoney, не видны потоку, который выполняет метод sumAccounts. Я думал, замок гарантирует, что это произойдет до отношений. Вы можете попробовать сделать поле баланса нестабильным. (Я предполагаю, что в других частях вашего кода нет ошибок. - person miskender; 06.04.2019
comment
sumAccounts действительно выполняется в новом Thread объекте при запуске. В конце концов, я заставил его работать, но производительность все еще очень плохая, как и все, что до сих пор работало. - person pr0f3ss; 06.04.2019
comment
@ pr0f3ss Что Вы сделали, чтобы он заработал. Это должно работать лучше, чем Reentrantlock. - person miskender; 06.04.2019
comment
Раньше я использовал rwl.readLock.lock() на обоих (я знаю, тупой). Хитрость заключалась в том, чтобы создать блокировку внутри класса Account и оттуда блокировать и разблокировать. Итак, в первом для каждого цикла вы блокируете учетную запись, затем добавляете ее баланс к сумме и, наконец, в следующем для каждого цикла вы разблокируете все учетные записи в том же порядке. Это гарантирует, что никакие транзакции не происходят одновременно. Как вы думаете, уместно ли ответить на мой вопрос, чтобы ответить на этот вопрос? - person pr0f3ss; 07.04.2019
comment
@ pr0f3ss Вы можете ответить на Ваши вопросы. Но rwl уже гарантирует, что при выполнении sumAccounts ни один метод transferMoney не может быть выполнен. (Для ясности; .... Ваш текущий код здесь в этом ответе относится к точному коду вашего метода transferMoney в вопросе.) - person miskender; 07.04.2019

Как указано в комментарии, блокировка объекта класса не приведет к блокировке всех экземпляров этого класса, а просто заблокирует объект Class, представляющий ваш класс Account. Эта блокировка не несовместима с блокировками на объектах Account, поэтому у вас вообще не происходит синхронизация.

Взятие блокировок на отдельные объекты Account можно выполнить внутри цикла for (в sumAccounts), но это не предотвратит выполнение таких расписаний:

- sumAccounts locks 'first' Account and reads balance (and releases lock again at end of the synchronized block taking the lock)
- system schedules a moneyTransfer() from 'first' to 'last'
- sumAccounts locks 'last' Account and reads balance, which includes the amount that was just transferred from 'first' and was already included in the sum

Поэтому, если вы хотите предотвратить это, вам также необходимо синхронизировать обработку moneyTransfer () в Account.class (что затем устраняет необходимость блокировки отдельных объектов).

person Erwin Smout    schedule 06.04.2019
comment
Ты прав. Но, синхронизируя оба метода через одну и ту же блокировку, я получаю худшую производительность, чем при их последовательном выполнении. (См. Вопрос - Что я пробовал так далеко - Пункты 3 и 4 маркированного списка) - person pr0f3ss; 06.04.2019
comment
КОНЕЧНО производительность у вас хуже (в частности, синдром конвоя). Это просто цена за лучшую целостность (и лучшую гарантию правильности результата). Основная проблема с блокировками java, удерживаемыми блоками synchronized (), заключается в том, что они всегда являются исключительно эксклюзивными блокировками (в java не было никакой концепции, такой как общие блокировки, которые есть в СУБД, что позволяет им достичь более высокого параллелизма при сохранении лучшего гарантии целостности). Слишком много, чтобы сказать по этому поводу по сравнению с тем, что помещается здесь. - person Erwin Smout; 06.04.2019
comment
Смотрите другой ответ на ReadWriteLock. (И обратите внимание, что если вы будете делать все это с использованием технологии баз данных, вы получите все это бесплатно, без необходимости писать одну единственную строку кода и часто без необходимости тратить ни секунды на размышления о том, какой тип блокировки или что-то подобное. вопросы. - person Erwin Smout; 06.04.2019
comment
Да, я знаю, что у меня будет худшая производительность, но даже когда я запускаю transferMoney() одновременно по отдельности (без sumAccounts()), я получаю примерно в три-четыре раза худшую производительность. - person pr0f3ss; 07.04.2019

Очень сложно просмотреть ваш код, потому что у нас нет возможности узнать, являются ли учетные записи объектов, с которыми вы синхронизируете, одними и теми же экземплярами во всех функциях.
Прежде всего, мы должны согласовать, будет ли сумма балансов и перевод сумм - это две операции, которые должны выполняться одновременно.
Я ожидаю, что сумма остатков будет одинаковой до и после перевода сумм.
Кроме того, вы используете synchronized(Account.class) в сумме балансов, что неверно. Вы должны синхронизировать объекты, которые вы перебираете в цикле.
Теперь, даже если вы действительно координируете одни и те же экземпляры, вы все равно можете иметь следующее расписание:

Thread-1 (transfer)  
  locks from  
Thread-2 (sum balance)  
  locks first object in the list and adds the balance to the running sum and moves to next object
Thread-1  
   locks to (which is the object Thread-2) processed
   moves money from => to  

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

Проблема в том, что вы обновляете 2 объекта в передаче, но блокируете только 1 в сумме.
Я бы посоветовал либо:

  1. либо синхронизировать оба метода на одной и той же блокировке и заставить их запускаться последовательно
  2. установить какой-то грязный флаг, когда объекты попадают в метод transfer, и если он установлен, пропустить их в сумме баланса и закончить сумму, когда все обновления будут выполнены
  3. Почему вы вообще это делаете на Java? Это должно происходить в базе данных с использованием транзакций со свойствами ACID.
person Cratylus    schedule 07.04.2019