1. Overview
1.概述
In this article, we’ll be looking at the Multiverse library – which helps us to implement the concept of Software Transactional Memory in Java.
在这篇文章中,我们将关注Multiverse库–它可以帮助我们在Java中实现软件事务性内存的概念。
Using constructs out of this library, we can create a synchronization mechanism on shared state – which is more elegant and readable solution than the standard implementation with the Java core library.
使用这个库中的构造,我们可以在共享状态上创建一个同步机制–这比使用Java核心库的标准实现更优雅、更易读。
2. Maven Dependency
2.Maven的依赖性
To get started we’ll need to add the multiverse-core library into our pom:
为了开始工作,我们需要将multiverse-core库加入我们的pom。
<dependency>
<groupId>org.multiverse</groupId>
<artifactId>multiverse-core</artifactId>
<version>0.7.0</version>
</dependency>
3. Multiverse API
3.多元宇宙API
Let’s start with some of the basics.
让我们从一些基本的东西开始。
Software Transactional Memory (STM) is a concept ported from the SQL database world – where each operation is executed within transactions that satisfy ACID (Atomicity, Consistency, Isolation, Durability) properties. Here, only Atomicity, Consistency and Isolation are satisfied because the mechanism runs in-memory.
软件事务性内存(STM)是一个从SQL数据库世界移植过来的概念–其中每个操作都在满足ACID(原子性、一致性、隔离性、持久性)属性的事务中执行。在这里,只有原子性、一致性和隔离性得到满足,因为该机制在内存中运行。
The main interface in the Multiverse library is the TxnObject – each transactional object needs to implement it, and the library provides us with a number of specific subclasses we can use.
Multiverse库中的主要接口是TxnObject –每个交易对象都需要实现它,并且该库为我们提供了许多可以使用的具体子类。
Each operation that needs to be placed within a critical section, accessible by only one thread and using any transactional object – needs to be wrapped within the StmUtils.atomic() method. A critical section is a place of a program that cannot be executed by more than one thread simultaneously, so access to it should be guarded by some synchronization mechanism.
每个需要放在关键部分的操作,只有一个线程可以访问,并且使用任何事务性对象–需要被包裹在StmUtils.atomic()方法中。关键部分是程序中一个不能被多个线程同时执行的地方,所以对它的访问应该由一些同步机制来保护。
If an action within a transaction succeeds, the transaction will be committed, and the new state will be accessible to other threads. If some error occurs, the transaction will not be committed, and therefore the state will not change.
如果一个事务中的操作成功了,该事务将被提交,新的状态将被其他线程访问。如果发生一些错误,该事务将不会被提交,因此状态不会改变。
Finally, if two threads want to modify the same state within a transaction, only one will succeed and commit its changes. The next thread will be able to perform its action within its transaction.
最后,如果两个线程想在一个事务中修改同一个状态,只有一个线程会成功并提交其修改。下一个线程将能够在其事务中执行其行动。
4. Implementing Account Logic Using STM
4.使用STM实现账户逻辑
Let’s now have a look at an example.
我们现在来看看一个例子。
Let’s say that we want to create a bank account logic using STM provided by the Multiverse library. Our Account object will have the lastUpadate timestamp that is of a TxnLong type, and the balance field that stores current balance for a given account and is of the TxnInteger type.
假设我们想使用Multiverse库提供的STM来创建一个银行账户逻辑。我们的Account对象将有lastUpadate时间戳,它是TxnLong类型,以及balance字段,它存储特定账户的当前余额,是TxnInteger类型。
The TxnLong and TxnInteger are classes from the Multiverse. They must be executed within a transaction. Otherwise, an exception will be thrown. We need to use the StmUtils to create new instances of the transactional objects:
TxnLong和TxnInteger是来自Multiverse的类。它们必须在一个事务中执行。否则,将抛出一个异常。我们需要使用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);
}
}
Next, we’ll create the adjustBy() method – which will increment the balance by the given amount. That action needs to be executed within a transaction.
接下来,我们将创建adjustBy()方法–它将使余额增加给定的金额。这个动作需要在一个交易中执行。
If any exception is thrown inside of it, the transaction will end without committing any change:
如果在其中抛出任何异常,事务将在不提交任何改变的情况下结束。
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");
}
});
}
If we want to get the current balance for the given account, we need to get the value from the balance field, but it also needs to be invoked with atomic semantics:
如果我们想获得给定账户的当前余额,我们需要从余额字段中获得数值,但它也需要以原子语义来调用。
public Integer getBalance() {
return balance.atomicGet();
}
5. Testing the Account
5.测试账户
Let’s test our Account logic. First, we want to decrement balance from the account by the given amount simply:
让我们测试一下我们的Account逻辑。首先,我们想从账户中减去给定金额的余额。
@Test
public void givenAccount_whenDecrement_thenShouldReturnProperValue() {
Account a = new Account(10);
a.adjustBy(-5);
assertThat(a.getBalance()).isEqualTo(5);
}
Next, let’s say that we withdraw from the account making the balance negative. That action should throw an exception, and leave the account intact because the action was executed within a transaction and was not committed:
接下来,让我们假设我们从账户中提款,使余额为负数。这个动作应该抛出一个异常,并使账户保持不变,因为这个动作是在一个事务中执行的,没有提交。
@Test(expected = IllegalArgumentException.class)
public void givenAccount_whenDecrementTooMuch_thenShouldThrow() {
// given
Account a = new Account(10);
// when
a.adjustBy(-11);
}
Let’s now test a concurrency problem that can arise when two threads want to decrement a balance at the same time.
现在我们来测试一个并发问题,当两个线程想同时递减一个余额时,可能会出现这个问题。
If one thread wants to decrement it by 5 and the second one by 6, one of those two actions should fail because the current balance of the given account is equal to 10.
如果一个线程想把它减去5,第二个线程想把它减去6,这两个动作中的一个应该失败,因为给定账户的当前余额等于10。
We’re going to submit two threads to the ExecutorService, and use the CountDownLatch to start them at the same time:
我们将向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);
}
});
After staring both actions at the same time, one of them will throw an exception:
同时盯着这两个动作后,其中一个会抛出一个异常。
countDownLatch.countDown();
ex.awaitTermination(1, TimeUnit.SECONDS);
ex.shutdown();
assertTrue(exceptionThrown.get());
6. Transferring from One Account to Another
6.从一个账户转到另一个账户
Let’s say that we want to transfer money from one account to the other. We can implement the transferTo() method on the Account class by passing the other Account to which we want to transfer the given amount of money:
假设我们想把钱从一个账户转到另一个账户。我们可以在Account类上实现transferTo()方法,通过传递另一个Account,我们想把给定的钱转到这个账户。
public void transferTo(Account other, int amount) {
StmUtils.atomic(() -> {
long date = System.currentTimeMillis();
adjustBy(-amount, date);
other.adjustBy(amount, date);
});
}
All logic is executed within a transaction. This will guarantee that when we want to transfer an amount that is higher than the balance on the given account, both accounts will be intact because the transaction will not commit.
所有的逻辑都在一个交易中执行。这将保证当我们想转移一个高于给定账户余额的金额时,两个账户将保持完整,因为交易不会提交。
Let’s test transferring logic:
让我们测试一下转移的逻辑。
Account a = new Account(10);
Account b = new Account(10);
a.transferTo(b, 5);
assertThat(a.getBalance()).isEqualTo(5);
assertThat(b.getBalance()).isEqualTo(15);
We simply create two accounts, we transfer the money from one to the other, and everything works as expected. Next, let’s say that we want to transfer more money than is available on the account. The transferTo() call will throw the IllegalArgumentException, and the changes will not be committed:
我们只需创建两个账户,将钱从一个账户转到另一个账户,一切都按预期进行。接下来,让我们说,我们想转移比账户上可用的更多的钱。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);
Note that the balance for both a and b accounts is the same as before the call to the transferTo() method.
注意,a和b账户的余额与调用transferTo()方法之前相同。
7. STM Is Deadlock Safe
7.STM是死锁安全的
When we’re using the standard Java synchronization mechanism, our logic can be prone to deadlocks, with no way to recover from them.
当我们使用标准的Java同步机制时,我们的逻辑可能很容易出现死锁,而且没有办法从中恢复。
The deadlock can occur when we want to transfer the money from account a to account b. In standard Java implementation, one thread needs to lock the account a, then account b. Let’s say that, in the meantime, the other thread wants to transfer the money from account b to account a. The other thread locks account b waiting for an account a to be unlocked.
当我们想把钱从账户a转到账户b时,就会出现死锁。在标准的Java实现中,一个线程需要锁定账户a,然后是账户b。假设在此期间,另一个线程想把钱从账户b转移到账户a。另一个线程锁定了账户b,等待账户a被解锁。
Unfortunately, the lock for an account a is held by the first thread, and the lock for account b is held by the second thread. Such situation will cause our program to block indefinitely.
不幸的是,账户a的锁由第一个线程持有,而账户b的锁则由第二个线程持有。这种情况将导致我们的程序无限期地阻塞。
Fortunately, when implementing transferTo() logic using STM, we do not need to worry about deadlocks as the STM is Deadlock Safe. Let’s test that using our transferTo() method.
幸运的是,当使用STM实现transferTo()逻辑时,我们不需要担心死锁问题,因为STM是死锁安全的。让我们用我们的transferTo()方法来测试一下。
Let’s say that we have two threads. First thread wants to transfer some money from account a to account b, and the second thread wants to transfer some money from account b to account a. We need to create two accounts and start two threads that will execute the transferTo() method in the same time:
比方说,我们有两个线程。第一个线程想从账户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);
});
After starting processing, both accounts will have the proper balance field:
开始处理后,两个账户都会有适当的余额栏。
countDownLatch.countDown();
ex.awaitTermination(1, TimeUnit.SECONDS);
ex.shutdown();
assertThat(a.getBalance()).isEqualTo(1);
assertThat(b.getBalance()).isEqualTo(19);
8. Conclusion
8.结论
In this tutorial, we had a look at the Multiverse library and at how we can use that to create lock-free and thread safe logic utilizing concepts in the Software Transactional Memory.
在本教程中,我们看了一下Multiverse库,以及如何利用它来创建无锁和线程安全的逻辑,利用软件事务存储器中的概念。
We tested the behavior of the implemented logic and saw that the logic that uses the STM is deadlock-free.
我们测试了所实现的逻辑的行为,看到使用STM的逻辑是无死锁的。
The implementation of all these examples and code snippets can be found in the GitHub project – this is a Maven project, so it should be easy to import and run as it is.
所有这些例子和代码片段的实现都可以在GitHub项目中找到–这是一个Maven项目,所以应该很容易导入并按原样运行。