Софтуерна транзакционна памет в Java, използваща Multiverse

1. Общ преглед

В тази статия ще разгледаме библиотеката Multiverse - която ни помага да реализираме концепцията за софтуерна транзакционна памет в Java.

Използвайки конструкции извън тази библиотека, можем да създадем механизъм за синхронизация на споделено състояние - което е по-елегантно и четимо решение от стандартното изпълнение с ядрената библиотека на Java.

2. Зависимост на Maven

За да започнем, ще трябва да добавим многоядрената библиотека в нашия пом:

 org.multiverse multiverse-core 0.7.0 

3. API на Multiverse

Нека започнем с някои от основите.

Софтуерната транзакционна памет (STM) е концепция, пренесена от света на базата данни на SQL - където всяка операция се изпълнява в рамките на транзакции, които удовлетворяват свойствата на ACID (атомност, последователност, изолация, трайност) . Тук са удовлетворени само атомността, последователността и изолацията, тъй като механизмът работи в паметта.

Основният интерфейс в библиотеката Multiverse е TxnObject - всеки транзакционен обект трябва да го реализира и библиотеката ни предоставя редица специфични подкласове, които можем да използваме.

Всяка операция, която трябва да бъде поставена в критична секция, достъпна само от една нишка и използваща всеки транзакционен обект, трябва да бъде обгърната в метода StmUtils.atomic () . Критичният раздел е място на програма, което не може да бъде изпълнено едновременно от повече от една нишка, така че достъпът до нея трябва да бъде защитен от някакъв механизъм за синхронизация.

Ако действие в рамките на транзакция успее, транзакцията ще бъде ангажирана и новото състояние ще бъде достъпно за други нишки. Ако възникне някаква грешка, транзакцията няма да бъде извършена и следователно състоянието няма да се промени.

И накрая, ако две нишки искат да модифицират едно и също състояние в рамките на транзакция, само една ще успее и ще извърши нейните промени. Следващата нишка ще може да извърши своето действие в рамките на своята транзакция.

4. Внедряване на логика на акаунта с помощта на STM

Нека сега разгледаме един пример .

Да кажем, че искаме да създадем логика на банкова сметка, използвайки STM, предоставена от библиотеката Multiverse . Обектът ни акаунт ще има клеймо за време lastUpadate, което е от тип TxnLong , и полето за баланс, което съхранява текущото салдо за даден акаунт и е от типа TxnInteger .

В TxnLong и TxnInteger са класове от Мултивселена . Те трябва да бъдат изпълнени в рамките на транзакция. В противен случай ще бъде създадено изключение. Трябва да използваме StmUtils, за да създадем нови екземпляри на транзакционните обекти:

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

След това ще създадем метода AdjustBy () , който ще увеличи баланса с дадената сума. Това действие трябва да бъде изпълнено в рамките на транзакция.

Ако в него бъде хвърлено някакво изключение, транзакцията ще приключи, без да се извърши промяна:

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"); } }); }

Ако искаме да получим текущото салдо за дадения акаунт, трябва да получим стойността от полето на баланса, но също така трябва да бъде извикано с атомна семантика:

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

5. Тестване на акаунта

Нека тестваме логиката на нашия акаунт . Първо, искаме да намалим баланса от сметката с дадената сума просто:

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

След това нека кажем, че теглим от сметката, като балансът е отрицателен. Това действие трябва да хвърли изключение и да остави акаунта непокътнат, тъй като действието е изпълнено в рамките на транзакция и не е извършено:

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

Нека сега тестваме проблем със съвпадението, който може да възникне, когато две нишки искат да намалят баланса едновременно.

Ако една нишка иска да я намали с 5, а втората с 6, едно от тези две действия трябва да се провали, тъй като текущото салдо на дадения акаунт е равно на 10.

Ще изпратим две нишки на ExecutorService и ще използваме CountDownLatch, за да ги стартираме едновременно:

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); } });

След като гледа едновременно и двете действия, едно от тях ще изведе изключение:

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

6. Прехвърляне от един акаунт в друг

Да кажем, че искаме да прехвърлим пари от едната сметка в другата. Ние можем да изпълни transferTo () метода на профил класа чрез преминаване на друга сметка , на които искаме да прехвърли определен размера на парите:

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

Цялата логика се изпълнява в рамките на транзакция. Това ще гарантира, че когато искаме да преведем сума, която е по-висока от салдото по дадения акаунт, и двата акаунта ще бъдат непокътнати, защото транзакцията няма да се ангажира.

Нека тестваме логиката за прехвърляне:

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

Просто създаваме два акаунта, прехвърляме парите от единия в другия и всичко работи както се очаква. След това нека кажем, че искаме да преведем повече пари, отколкото е налично по сметката. В transferTo () повикване ще хвърли IllegalArgumentException, и промените няма да бъдат извършени:

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);

Имайте предвид, че салдото и за акаунтите a и b е същото като преди извикването на метода transferTo () .

7. STM е в безопасност

Когато използваме стандартния механизъм за синхронизация на Java, нашата логика може да бъде склонна към блокировки, без начин да се възстанови от тях.

Блокирането може да възникне, когато искаме да преведем парите от сметка а на сметка б . При стандартното изпълнение на Java една нишка трябва да заключи акаунта a , след това акаунт b . Да кажем, че междувременно другата нишка иска да прехвърли парите от сметка b в сметка a . Другата нишка заключва акаунт b в очакване акаунт a да бъде отключен.

За съжаление, заключването за акаунт a се държи от първата нишка, а заключването за акаунт b се държи от втората нишка. Такава ситуация ще накара нашата програма да блокира за неопределено време.

За щастие, когато внедряваме логиката transferTo () с помощта на STM, не е нужно да се притесняваме за блокировки, тъй като STM е Deadlock Safe. Нека тестваме това, използвайки нашия метод transferTo () .

Да кажем, че имаме две нишки. Първата нишка иска да прехвърли малко пари от сметка a към сметка b , а втората нишка иска да прехвърли малко пари от сметка b към сметка a . Трябва да създадем два акаунта и да стартираме две нишки, които да изпълнят метода transferTo () едновременно:

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); });

След започване на обработката и двата акаунта ще имат правилното поле за баланс:

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

8. Заключение

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

Тествахме поведението на внедрената логика и видяхме, че логиката, която използва STM, не е в задънена улица.

Изпълнението на всички тези примери и кодови фрагменти може да се намери в проекта GitHub - това е проект на Maven, така че трябва да е лесно да се импортира и да се изпълнява както е.