web-dev-qa-db-fra.com

Synchronisation Java: déplacer de l’argent de façon atomique entre des paires de comptes?

Comment gagner de l'argent en passant d'un compte à un autre atomique? Pour:

public class Account {
    public Account(BigDecimal initialAmount) {...}
    public BigDecimal getAmount() {...}
    public void setAmount(BigDecimal amount) {...}
}

Je m'attends à ce pseudo-code:

public boolean transfer(Account from, Account to, BigDecimal amount) {
    BigDecimal fromValue = from.getAmount();
    if (amount.compareTo(fromValue) < 0)
         return false;
    BigDecimal toValue = to.getAmount();
    from.setAmount(fromValue.add(amount.negate()));
    to.setAmount(toValue.add(amount));
    return true;
}

mettre à jour des comptes en toute sécurité dans un environnement multithreading, je vois le cas de danger comme suit:

acc1 --> acc2  ||  acc2 --> acc1
acc1 --> acc2  ||  acc2 --> acc3  ||  acc3 --> acc1
...

La solution la plus simple consiste à bloquer les objets partagés, mais cela sera inefficace dans les cas suivants:

acc1 --> acc2  ||  acc3 --> acc4  and  acc1 != acc3 and acc2 != acc4

Je m'attends à ce que les mouvements indépendants soient effectués en parallèle.

UPDATESemble cette solution suggérée:

synchronize (acc1) {
   synchronize (acc2) {
     ....
   }
}

conduire à l'impasse que 2 serrures acquises séquentiellement ... 

UPDATE 2 que voulez-vous dire exactement par "mettre à jour des comptes en toute sécurité dans un environnement multithreading"? Le seul souci est-il que les comptes ne finissent pas par avoir moins de fonds ou y a-t-il un autre problème?

Si acc1(2); acc2(3) et acc1 --1--> acc2 et acc2 --2--> acc1 j'attends de la cohérence: (acc1, acc2) a la valeur (3, 2), mais pas (4, 2) ni (3, 4). Le total devrait être 5 et non pas 1 + 3 = 4 ou 4 + 3 = 7.

combien de transactions simultanées attendez-vous à la fois? 1000-10000 - le verrouillage d'un objet partagé n'est donc pas efficace.

43
gavenkoa

Une solution simple pourrait être d’utiliser un verrou par compte, mais pour éviter un blocage, vous devez toujours acquérir des verrous dans le même ordre. Ainsi, vous pouvez avoir un ID de compte final et obtenir le verrou du compte avec un identifiant inférieur en premier:

public void transfer(Account acc1, Account acc2, BigDecimal value) {
    Object lock1 = acc1.ID < acc2.ID ? acc1.LOCK : acc2.LOCK;
    Object lock2 = acc1.ID < acc2.ID ? acc2.LOCK : acc1.LOCK;
    synchronized (lock1) {
       synchronized (lock2) {
          acc1.widrawal(value);
          acc2.send(value);
       }
    }
}
43
Petr

Une façon de faire est d’avoir un journal des transactions. Avant de transférer l'argent, vous devez indiquer dans le journal des transactions de chaque compte ce que vous avez l'intention de faire. Le journal doit contenir: la quantité d'argent qui a été prise dans/hors du compte et un verrou partagé entre la paire de journaux.

Initialement, le verrou devrait être bloqué. Vous avez créé la paire de journaux, l'un avec un montant de X et l'autre avec un montant de -X, et les deux partagent un verrou. Envoyez ensuite l’entrée du journal dans la boîte de réception des comptes respectifs, le compte sur lequel l’argent est prélevé doit réserver ce montant. Une fois que vous avez confirmé qu'ils sont livrés en toute sécurité, relâchez le verrou. Au moment où le verrou est libéré, vous êtes à un point s'il n'y a pas de retour. Les comptes devraient alors se résoudre eux-mêmes.

Si l'une des parties souhaite faire échouer la transaction à tout moment avant que le verrou ne soit libéré, supprimez simplement les journaux et renvoyez le montant réservé dans le solde principal.

Cette approche est peut-être un peu lourde, mais elle fonctionnerait également dans un scénario distribué où les comptes sont en réalité sur des machines différentes, et les boîtes de réception devraient en fait être conservées, afin que de l'argent ne soit jamais perdu si une machine tombe en panne hors ligne de manière inattendue. Sa technique générale s'appelle le verrouillage en deux phases.

12
Lie Ryan

Je proposerais de créer une méthode Account.withdraw (montant) qui lève une exception si elle ne dispose pas de fonds suffisants. Cette méthode doit être synchronisée sur le compte lui-même.

Modifier:

Il doit également exister une méthode Account.deposit (montant) qui est synchronisée sur l'instance de compte destinataire.

Fondamentalement, cela entraînera un verrou du premier compte lors du retrait, puis un autre verrou sur le compte destinataire lors du dépôt. Donc deux serrures mais pas en même temps.

Exemple de code: / Suppose que les retraits/dépôts sont synchronisés et renvoient le statut de réussite booléen plutôt que de lever une exception.

public boolean transfer(Account from, Account to, BigDecimal amount) {
    boolean success = false;
    boolean withdrawn = false;
    try {
        if (from.withdraw(amount)) {
            withdrawn = true;
            if (to.deposit(amount)) {
                success = true;
            }
        }
    } finally {
        if (withdrawn && !success) {
            from.deposit(amount);
        }
    }

    return success;
}
8
Christian

Vous pouvez créer une variable supplémentaire AccountT qui existe uniquement pour le transfert de l'argent. Ainsi, si vous souhaitez passer de A à B, vous passez effectivement de A à T, puis de T à B. Pour chacun de ces transferts, vous ne verrouillez que A ou B en fonction du compte participant au transfert. Étant donné que vous utilisez le même type pour les transferts, vous vous retrouvez avec peu de code supplémentaire et donc de faibles coûts de maintenance.

Pour réduire le nombre de comptes supplémentaires, vous pouvez les conserver dans un pool. Si un pool de threads traite des transferts, vous pouvez affecter à chaque thread son propre compte supplémentaire. Par conséquent, vous n'avez pas besoin de demander et de libérer trop souvent ces comptes supplémentaires d'un pool.

7
SpaceTrucker

Une approche consiste à utiliser une sorte de "verrou rayé" avec des méthodes de verrouillage/déverrouillage opérant sur plusieurs verrous. Les comptes sont mappés aux verrous à l'aide de hashCode, plus vous allouez de verrous, plus vous obtenez de parallélisme.

Voici un exemple de code:

public class StripedLock {

    private final NumberedLock[] locks;

    private static class NumberedLock {
        private final int id;
        private final ReentrantLock lock;

        public NumberedLock(int id) {
            this.id = id;
            this.lock = new ReentrantLock();
        }
    }


    /**
     * Default ctor, creates 16 locks
     */
    public StripedLock() {
        this(4);
    }

    /**
     * Creates array of locks, size of array may be any from set {2, 4, 8, 16, 32, 64}
     * @param storagePower size of array will be equal to <code>Math.pow(2, storagePower)</code>
     */
    public StripedLock(int storagePower) {
        if (!(storagePower >= 1 && storagePower <= 6)) { throw new IllegalArgumentException("storage power must be in [1..6]"); }

        int lockSize = (int) Math.pow(2, storagePower);
        locks = new NumberedLock[lockSize];
        for (int i = 0; i < locks.length; i++)
            locks[i] = new NumberedLock(i);
    }

    /**
     * Map function between integer and lock from locks array
     * @param id argument
     * @return lock which is result of function
     */
    private NumberedLock getLock(int id) {
        return locks[id & (locks.length - 1)];
    }

    private static final Comparator<? super NumberedLock> CONSISTENT_COMPARATOR = new Comparator<NumberedLock>() {
        @Override
        public int compare(NumberedLock o1, NumberedLock o2) {
            return o1.id - o2.id;
        }
    };


    public void lockIds(@Nonnull int[] ids) {
        Preconditions.checkNotNull(ids);
        NumberedLock[] neededLocks = getOrderedLocks(ids);
        for (NumberedLock nl : neededLocks)
            nl.lock.lock();
    }

    public void unlockIds(@Nonnull int[] ids) {
        Preconditions.checkNotNull(ids);
        NumberedLock[] neededLocks = getOrderedLocks(ids);
        for (NumberedLock nl : neededLocks)
            nl.lock.unlock();
    }

    private NumberedLock[] getOrderedLocks(int[] ids) {
        NumberedLock[] neededLocks = new NumberedLock[ids.length];
        for (int i = 0; i < ids.length; i++) {
            neededLocks[i] = getLock(i);
        }
        Arrays.sort(neededLocks, CONSISTENT_COMPARATOR);
        return neededLocks;
    }
}

    // ...
    public void transfer(StripedLock lock, Account from, Account to) {
        int[] accountIds = new int[]{from.getId(), to.getId()};
        lock.lockIds(accountIds);
        try {
            // profit!
        } finally {
            lock.unlockIds(accountIds);
        }
    }
6
Victor Sorokin

Comme indiqué précédemment, vous devez verrouiller les deux comptes, toujours dans le même ordre. Cependant, l’essentiel est d’assurer une granularité et une singularité élevées dans l’instance VM. Cela peut être fait en utilisant String.intern() :

public boolean transfer(Account from, Account to, BigDecimal amount) {
    String fromAccountId = from.id.toString().intern();
    String toAccountId = to.id.toString().intern();
    String lock1, lock2;

    if (from.id < to.id) {
       lock1 = fromAccountId;
       lock2 = toAccountId;
    } else {
       lock1 = toAccountId;
       lock2 = fromAccountId;
    }

    // synchronizing from this point, since balances are checked
    synchronized(lock1) {
        synchronized(lock2) {
            BigDecimal fromValue = from.getAmount();
            if (amount.compareTo(fromValue) < 0)
                 return false;
            BigDecimal toValue = to.getAmount();
            from.setAmount(fromValue.add(amount.negate()));
            to.setAmount(toValue.add(amount));
            return true;
        }
    }
}
4
Leonardo Braga

N'utilisez pas la synchronisation intégrée, utilisez un objet verrouillé. Utilisez tryLock () pour obtenir un verrou exclusif sur les deux comptes en même temps. Si l'un des deux échoue, relâchez les deux verrous, attendez une durée aléatoire et réessayez.

4
Jesse Barnum

Comme vous l'avez mentionné, il y aura entre 1 000 et 1 000 transactions simultanées attendues à la fois, ce qui vous permettra de stocker des comptes sur lesquels des transactions sont en cours et de gérer les accès simultanés.

Une solution consiste à autoriser le système à créer un seul objet avec un identifiant de compte particulier, ce qui signifie que si vous souhaitez effectuer une transaction entre les comptes "123" et "456", votre thread créera un objet de compte et dans ce constructeur de la classe de comptes, nous allons: Vérifiez si un autre objet du compte existe avec un identifiant de compte particulier, si un autre objet de compte existe avec le même identifiant de compte, cela signifie qu'une transaction est en cours avec un identifiant de compte particulier, vous devez donc attendre pour obtenir l'objet du compte.

Donc, nous pouvons faire une transaction entre "123" et "456" et en même temps, nous pouvons faire une transaction entre "abc" et "xyz" mais si en même temps un autre thread essaie de créer un objet du compte "123", le système ne le fera pas. dites s'il vous plaît attendez 

pour référence, vous pouvez voir ci-dessous le code 

Notez s'il vous plaît :

  1. ne pas oublier de supprimer votre identifiant de compte de la carte des verrous par appel à freeAccount (BigDecimal accId) de la classe LockHolder

  2. J'ai utilisé HasMap instand of list car list ne sera pas un bon choix si vous enlevez un élément de manière aléatoire (ou si vous le mettez à jour fréquemment) 

    package test;
    
    import Java.math.BigDecimal;
    import Java.util.HashMap;
    import Java.util.Map;
    
    public class T {
    
    public static void main(String[] args) {
        Account ac, ac2;
    
        try {
            ac = new Account(new BigDecimal("123"));
        } catch (Exception e) {
            e.printStackTrace();
        }
        try {
            ac2 = new Account(new BigDecimal("123"));
        } catch (Exception e) {
            System.out.println("Please Wait");
        }
    
    }
    }
    
    class Account {
     public Account(BigDecimal accId) throws Exception {
        if (LockHolder.isLocked(accId)) {
            throw new Exception();
        } else {
            LockHolder.setLock(accId);
        }
     }
    }
    
    class LockHolder {
     public static  Map<BigDecimal, Integer> locks = new HashMap<BigDecimal, Integer>();
    
     public synchronized static boolean isLocked(BigDecimal accId) {
        return LockHolder.locks.containsKey(accId);
     }
    
     public synchronized static void setLock(BigDecimal accId) {
        LockHolder.locks.put(accId , 1);
     }
     public synchronized static void freeAccount(BigDecimal accId) {
        LockHolder.locks.remove(accId);
     }
    }
    
4
K M PATEL

Une approche qui restera robuste même si les threads peuvent être arbitrairement bloqués consiste à faire en sorte que chaque compte maintienne une liste des transactions demandées ou comptabilisées. Pour demander un transfert d'un compte à un autre, créez un objet de transaction définissant la demande et ajoutez-le à la file d'attente des demandes du compte source. Si ce compte peut effectuer la transaction, il doit le déplacer dans la liste des transactions enregistrées et l'ajouter à la file d'attente des demandes pour la destination. En utilisant AtomicReference, il est possible de s'assurer qu'à partir du moment où la transaction est placée dans la file d'attente du premier compte, l'état du système aura toujours ensuite la transaction en attente, terminée ou abandonnée, et même si certains ou tous les threads devaient passer outre, l’examen des listes de transactions permettrait de déterminer à quel argent appartenait où.

En revanche, lors de l’utilisation de verrous, les événements qui retardent inopinément un thread peuvent en entraver arbitrairement l’exécution, et si un thread est tué alors qu’il est verrouillé, il peut être impossible de déterminer ce qu’il avait fait ou n’avait pas fait auparavant. .

2
supercat

Merci à tous pour l'intérêt d'interroger.

J'ai trouvé plusieurs solutions dans https://www.securecoding.cert.org/confluence/display/Java/LCK07-J.+Avoid+deadlock+by+requesting+and+releasing+locks+in+the+same+ ordre

Comme une réponse de lien a été supprimée ici, il s’agit d’un élément de code essentiel qui aide tout le monde à la chute de cert.org. Les pièces sont longues, je n'ai donc inclus aucun avantage/inconvénient.

Objet de verrouillage final statique privé:

final class BankAccount {
  private double balanceAmount;  // Total amount in bank account
  private static final Object lock = new Object();

  BankAccount(double balance) {
    this.balanceAmount = balance;
  }

  // Deposits the amount from this object instance
  // to BankAccount instance argument ba
  private void depositAmount(BankAccount ba, double amount) {
    synchronized (lock) {
      if (amount > balanceAmount) {
        throw new IllegalArgumentException(
            "Transfer cannot be completed");
      }
      ba.balanceAmount += amount;
      this.balanceAmount -= amount;
    }
  }

  public static void initiateTransfer(final BankAccount first,
    final BankAccount second, final double amount) {

    Thread transfer = new Thread(new Runnable() {
        @Override public void run() {
          first.depositAmount(second, amount);
        }
    });
    transfer.start();
  }
}

Serrures commandées:

final class BankAccount implements Comparable<BankAccount> {
  private double balanceAmount;  // Total amount in bank account
  private final Object lock;

  private final long id; // Unique for each BankAccount
  private static long NextID = 0; // Next unused ID

  BankAccount(double balance) {
    this.balanceAmount = balance;
    this.lock = new Object();
    this.id = this.NextID++;
  }

  @Override public int compareTo(BankAccount ba) {
     return (this.id > ba.id) ? 1 : (this.id < ba.id) ? -1 : 0;
  }

  // Deposits the amount from this object instance
  // to BankAccount instance argument ba
  public void depositAmount(BankAccount ba, double amount) {
    BankAccount former, latter;
    if (compareTo(ba) < 0) {
      former = this;
      latter = ba;
    } else {
      former = ba;
      latter = this;
    }
    synchronized (former) {
      synchronized (latter) {
        if (amount > balanceAmount) {
          throw new IllegalArgumentException(
              "Transfer cannot be completed");
        }
        ba.balanceAmount += amount;
        this.balanceAmount -= amount;
      }
    }
  }

  public static void initiateTransfer(final BankAccount first,
    final BankAccount second, final double amount) {

    Thread transfer = new Thread(new Runnable() {
        @Override public void run() {
          first.depositAmount(second, amount);
        }
    });
    transfer.start();
  }
}

Solution conforme (ReentrantLock):

final class BankAccount {
  private double balanceAmount;  // Total amount in bank account
  private final Lock lock = new ReentrantLock();
  private final Random number = new Random(123L);

  BankAccount(double balance) {
    this.balanceAmount = balance;
  }

  // Deposits amount from this object instance
  // to BankAccount instance argument ba
  private void depositAmount(BankAccount ba, double amount)
                             throws InterruptedException {
    while (true) {
      if (this.lock.tryLock()) {
        try {
          if (ba.lock.tryLock()) {
            try {
              if (amount > balanceAmount) {
                throw new IllegalArgumentException(
                    "Transfer cannot be completed");
              }
              ba.balanceAmount += amount;
              this.balanceAmount -= amount;
              break;
            } finally {
              ba.lock.unlock();
            }
          }
        } finally {
          this.lock.unlock();
        }
      }
      int n = number.nextInt(1000);
      int TIME = 1000 + n; // 1 second + random delay to prevent livelock
      Thread.sleep(TIME);
    }
  }

  public static void initiateTransfer(final BankAccount first,
    final BankAccount second, final double amount) {

    Thread transfer = new Thread(new Runnable() {
        public void run() {
          try {
            first.depositAmount(second, amount);
          } catch (InterruptedException e) {
            Thread.currentThread().interrupt(); // Reset interrupted status
          }
        }
    });
    transfer.start();
  }
}
0
gavenkoa