1. Обзор
В этой статье мы будем смотреть на Мультивселенная библиотека – которая помогает нам реализовать концепцию Программное обеспечение транзакционной памяти на Яве.
Используя конструкции из этой библиотеки, мы можем создать механизм синхронизации в общем состоянии, что является более элегантным и читаемым решением, чем стандартная реализация с библиотекой ядра Java.
2. Зависимость от Maven
Чтобы начать работу, нам нужно добавить мультивселенной библиотека в наш пом:
org.multiverse multiverse-core 0.7.0
3. Мультивселенная API
Начнем с некоторых основ.
Software Transactional Memory (STM) — это концепция, портированная из мира баз данных S’L, где каждая операция выполняется в транзакциях, удовлетворя ACID (Атоматичность, последовательность, изоляция, долговечность) свойства. Вот, только атоматичность, последовательность и изоляция удовлетворены, потому что механизм работает в памяти.
Основным интерфейсом в библиотеке Multiverse является TxnObject – каждый транзакционный объект должен реализовать его, и библиотека предоставляет нам ряд конкретных подклассов, которые мы можем использовать.
Каждая операция, которая должна быть помещена в критический раздел, доступный только одним потоком и использующий любой транзакционный объект, должна быть обернута в StmUtils.atomic() метод. Критический раздел является местом программы, которая не может быть выполнена более чем одним потоком одновременно, поэтому доступ к нему должен охраняться каким-то механизмом синхронизации.
Если действие в транзакции увенчается успехом, транзакция будет совершена, и новое состояние будет доступно другим потокам. Если происходит какая-то ошибка, транзакция не будет совершена, и поэтому состояние не изменится.
Наконец, если два потока хотят изменить одно и то же состояние в транзакции, только один из них получит успех и совершит его изменения. Следующий поток сможет выполнять свои действия в рамках транзакции.
4. Реализация логики учетной записи с использованием STM
Давайте теперь посмотрим на пример .
Допустим, мы хотим создать логику банковского счета с помощью STM, предоставленной Мультивселенная библиотека. Наша Учетная объект будет иметь последнийУпадат таймштамп, который имеет TxnLong типа и баланс поле, которое хранит текущий баланс для данного счета и имеет TxnInteger тип.
TxnLong и TxnInteger являются классы из Мультивселенная . Они должны быть выполнены в рамках транзакции. В противном случае будет брошено исключение. Мы должны использовать Стмутилс для создания новых экземпляров транзакционных объектов:
public class Account { private TxnLong lastUpdate; private TxnInteger balance; public Account(int balance) { this.lastUpdate = StmUtils.newTxnLong(System.currentTimeMillis()); this.balance = StmUtils.newTxnInteger(balance); } }
Далее мы создадим настроить() метод – который будет приращение баланса на предоставленную сумму. Это действие должно быть выполнено в рамках транзакции.
Если внутри него брошено какое-либо исключение, транзакция завершится без каких-либо изменений:
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. Тестирование учетной записи
Давайте проверят наши Учетная логика. Во-первых, мы хотим, чтобы decrement остаток со счета на предоставленную сумму просто:
@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); }
Давайте теперь тестируем проблему с эквивалентностью, которая может возникнуть, когда два потока хотят разработать баланс одновременно.
Если один поток хочет decrement его на 5, а второй на 6, один из этих двух действий должен потерпеть неудачу, потому что текущий баланс данного счета равен 10.
Мы собираемся представить две темы в ИсполнительСервис , и использовать 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 () вызов будет бросать НезаконноеАргументЭксцепция, и изменения не будут внесены:
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);
Обратите внимание, что баланс для обеих и b учетные записи такие же, как и до звонка в transferTo () метод.
7. STM является тупик безопасным
Когда мы используем стандартный механизм синхронизации Java, наша логика может быть подвержена тупикам, без способа восстановления от них.
Затор может возникнуть, когда мы хотим перевести деньги со счета для учета b . В стандартной реализации Java одному потоку необходимо заблокировать учетную запись , затем счет b . Допустим, что, в то же время, другой поток хочет перевести деньги со счета b для учета . Другой поток блокирует учетную запись b в ожидании учетной быть разблокирован.
К сожалению, блокировка учетной записи проводится первым потоком, а блокировка для учетной записи b проводится вторым потоком. Такая ситуация приведет к тому, что наша программа будет блокироваться на неопределенный срок.
К счастью, при реализации transferTo () логика использования STM, нам не нужно беспокоиться о тупиках, как STM является тупик безопасной. Давайте проверить, что с помощью наших transferTo () метод.
Допустим, у нас есть две нити. Первый поток хочет перевести деньги со счета для учета b , и второй поток хочет перевести деньги со счета b для учета . Нам нужно создать две учетные записи и начать два потока, которые будут выполнять 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. Заключение
В этом учебнике, мы имели взгляд на Мультивселенная библиотека и как мы можем использовать это для создания блокировки и потока безопасной логики, используя концепции в программном обеспечении транзакционной памяти.
Мы проверили поведение реализованной логики и увидели, что логика, которая использует STM, не имеет тупика.
Реализация всех этих примеров и фрагментов кода можно найти в Проект GitHub – это проект Maven, поэтому его нужно легко импортировать и запускать в том же объеме.