Блокировка всех экземпляров класса в Java - PullRequest
1 голос
/ 06 апреля 2019

Я внедряю параллельную банковскую систему, в которой все операции могут выполняться одновременно. Я реализовал поточно-безопасный метод 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() казни? Если нет, как я могу решить эту проблему?

Edit: Код для getBalance():

public int getBalance() {
        return balance;
}

Ответы [ 3 ]

1 голос
/ 06 апреля 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 (что затем устраняет необходимость блокировки отдельных объектов).

1 голос
/ 06 апреля 2019

Вы можете использовать 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

0 голосов
/ 07 апреля 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.
...