Mémoire transactionnelle logicielle en Java à l'aide de Multiverse

1. Vue d'ensemble

Dans cet article, nous examinerons la bibliothèque Multiverse - qui nous aide à implémenter le concept de Software Transactional Memory en Java.

En utilisant des constructions de cette bibliothèque, nous pouvons créer un mécanisme de synchronisation sur l'état partagé - qui est une solution plus élégante et plus lisible que l'implémentation standard avec la bibliothèque principale Java.

2. Dépendance de Maven

Pour commencer, nous devons ajouter la bibliothèque multiverse-core dans notre pom:

 org.multiverse multiverse-core 0.7.0 

3. API multivers

Commençons par quelques notions de base.

La mémoire transactionnelle logicielle (STM) est un concept porté à partir du monde de la base de données SQL - où chaque opération est exécutée dans des transactions qui satisfont aux propriétés ACID (atomicité, cohérence, isolation, durabilité) . Ici, seules l'atomicité, la cohérence et l'isolation sont satisfaites car le mécanisme s'exécute en mémoire.

L'interface principale de la bibliothèque Multiverse est le TxnObject - chaque objet transactionnel doit l'implémenter, et la bibliothèque nous fournit un certain nombre de sous-classes spécifiques que nous pouvons utiliser.

Chaque opération qui doit être placée dans une section critique, accessible par un seul thread et utilisant n'importe quel objet transactionnel - doit être encapsulée dans la méthode StmUtils.atomic () . Une section critique est un emplacement d'un programme qui ne peut pas être exécuté par plus d'un thread simultanément, donc l'accès à celui-ci doit être protégé par un mécanisme de synchronisation.

Si une action au sein d'une transaction réussit, la transaction sera validée et le nouvel état sera accessible à d'autres threads. Si une erreur se produit, la transaction ne sera pas validée et, par conséquent, l'état ne changera pas.

Enfin, si deux threads souhaitent modifier le même état dans une transaction, un seul réussira et validera ses modifications. Le thread suivant pourra effectuer son action dans sa transaction.

4. Implémentation de la logique de compte à l'aide de STM

Voyons maintenant un exemple .

Disons que nous voulons créer une logique de compte bancaire à l'aide de STM fourni par la bibliothèque Multiverse . Notre objet Account aura l' horodatage lastUpadate qui est de type TxnLong et le champ solde qui stocke le solde actuel pour un compte donné et est de type TxnInteger .

Le TxnLong et TxnInteger sont des classes du multivers . Ils doivent être exécutés dans une transaction. Sinon, une exception sera levée. Nous devons utiliser les StmUtils pour créer de nouvelles instances des objets transactionnels:

public class Account { private TxnLong lastUpdate; private TxnInteger balance; public Account(int balance) { this.lastUpdate = StmUtils.newTxnLong(System.currentTimeMillis()); this.balance = StmUtils.newTxnInteger(balance); } }

Ensuite, nous allons créer la méthode AdjustBy () - qui incrémentera le solde du montant donné. Cette action doit être exécutée dans une transaction.

Si une exception est levée à l'intérieur, la transaction se terminera sans valider aucune modification:

public void adjustBy(int amount) { adjustBy(amount, System.currentTimeMillis()); } public void adjustBy(int amount, long date) { StmUtils.atomic(() -> { balance.increment(amount); lastUpdate.set(date); if (balance.get() <= 0) { throw new IllegalArgumentException("Not enough money"); } }); }

Si nous voulons obtenir le solde actuel pour le compte donné, nous devons obtenir la valeur du champ balance, mais il doit également être appelé avec la sémantique atomique:

public Integer getBalance() { return balance.atomicGet(); }

5. Test du compte

Testons notre logique de compte . Tout d'abord, nous voulons décrémenter le solde du compte du montant donné simplement:

@Test public void givenAccount_whenDecrement_thenShouldReturnProperValue() { Account a = new Account(10); a.adjustBy(-5); assertThat(a.getBalance()).isEqualTo(5); }

Ensuite, disons que nous retirons du compte ce qui rend le solde négatif. Cette action doit lever une exception et laisser le compte intact car l'action a été exécutée dans une transaction et n'a pas été validée:

@Test(expected = IllegalArgumentException.class) public void givenAccount_whenDecrementTooMuch_thenShouldThrow() { // given Account a = new Account(10); // when a.adjustBy(-11); } 

Testons maintenant un problème de concurrence qui peut survenir lorsque deux threads veulent décrémenter un solde en même temps.

Si un thread veut le décrémenter de 5 et le second de 6, l'une de ces deux actions doit échouer car le solde actuel du compte donné est égal à 10.

Nous allons soumettre deux threads à ExecutorService et utiliser CountDownLatch pour les démarrer en même temps:

ExecutorService ex = Executors.newFixedThreadPool(2); Account a = new Account(10); CountDownLatch countDownLatch = new CountDownLatch(1); AtomicBoolean exceptionThrown = new AtomicBoolean(false); ex.submit(() -> { try { countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } try { a.adjustBy(-6); } catch (IllegalArgumentException e) { exceptionThrown.set(true); } }); ex.submit(() -> { try { countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } try { a.adjustBy(-5); } catch (IllegalArgumentException e) { exceptionThrown.set(true); } });

Après avoir lancé les deux actions en même temps, l'une d'elles lancera une exception:

countDownLatch.countDown(); ex.awaitTermination(1, TimeUnit.SECONDS); ex.shutdown(); assertTrue(exceptionThrown.get());

6. Transfert d'un compte à un autre

Disons que nous voulons transférer de l'argent d'un compte à l'autre. Nous pouvons implémenter la méthode transferTo () sur la classe Account en passant l'autre compte vers lequel nous voulons transférer le montant d'argent donné:

public void transferTo(Account other, int amount) { StmUtils.atomic(() -> { long date = System.currentTimeMillis(); adjustBy(-amount, date); other.adjustBy(amount, date); }); }

Toute la logique est exécutée dans une transaction. Cela garantira que lorsque nous voulons transférer un montant supérieur au solde du compte donné, les deux comptes seront intacts car la transaction ne sera pas validée.

Testons la logique de transfert:

Account a = new Account(10); Account b = new Account(10); a.transferTo(b, 5); assertThat(a.getBalance()).isEqualTo(5); assertThat(b.getBalance()).isEqualTo(15);

Nous créons simplement deux comptes, nous transférons l'argent de l'un à l'autre et tout fonctionne comme prévu. Ensuite, disons que nous voulons transférer plus d'argent que ce qui est disponible sur le compte. L' appel transferTo () lèvera l' exception IllegalArgumentException et les modifications ne seront pas validées:

try { a.transferTo(b, 20); } catch (IllegalArgumentException e) { System.out.println("failed to transfer money"); } assertThat(a.getBalance()).isEqualTo(5); assertThat(b.getBalance()).isEqualTo(15);

Notez que l'équilibre pour les deux a et b des comptes est la même qu'avant l'appel à la transferTo () méthode.

7. STM est sécurisé contre les impasses

Lorsque nous utilisons le mécanisme de synchronisation Java standard, notre logique peut être sujette à des blocages, sans aucun moyen de les récupérer.

L'impasse peut se produire lorsque nous voulons transférer l'argent du compte a vers le compte b . Dans l'implémentation Java standard, un thread doit verrouiller le compte a , puis le compte b . Disons que, entre-temps, l'autre thread souhaite transférer l'argent du compte b vers le compte a . L'autre thread verrouille le compte b en attente de déverrouillage d' un compte a .

Malheureusement, le verrou d'un compte a est détenu par le premier thread et le verrou du compte b est détenu par le second thread. Une telle situation entraînera le blocage de notre programme indéfiniment.

Heureusement, lors de l'implémentation de la logique transferTo () à l' aide de STM, nous n'avons pas à nous soucier des blocages car le STM est Deadlock Safe. Testons cela en utilisant notre méthode transferTo () .

Disons que nous avons deux fils. Le premier thread veut transférer de l'argent du compte a vers le compte b , et le second thread veut transférer de l'argent du compte b vers le compte a . Nous devons créer deux comptes et démarrer deux threads qui exécuteront la méthode transferTo () en même temps:

ExecutorService ex = Executors.newFixedThreadPool(2); Account a = new Account(10); Account b = new Account(10); CountDownLatch countDownLatch = new CountDownLatch(1); ex.submit(() -> { try { countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } a.transferTo(b, 10); }); ex.submit(() -> { try { countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } b.transferTo(a, 1); });

Après le début du traitement, les deux comptes auront le champ de solde approprié:

countDownLatch.countDown(); ex.awaitTermination(1, TimeUnit.SECONDS); ex.shutdown(); assertThat(a.getBalance()).isEqualTo(1); assertThat(b.getBalance()).isEqualTo(19);

8. Conclusion

Dans ce didacticiel, nous avons examiné la bibliothèque Multiverse et comment nous pouvons l'utiliser pour créer une logique sans verrouillage et sans fil en utilisant les concepts de la mémoire transactionnelle logicielle.

Nous avons testé le comportement de la logique implémentée et constaté que la logique qui utilise le STM est sans interblocage.

L'implémentation de tous ces exemples et extraits de code se trouve dans le projet GitHub - il s'agit d'un projet Maven, il devrait donc être facile à importer et à exécuter tel quel.