1. 概述
本文将探讨Multiverse库——一个帮助我们在Java中实现软件事务内存(STM)概念的强大工具。通过该库提供的构造,我们可以为共享状态创建同步机制,这比使用Java核心库的标准实现更优雅、更易读。
2. Maven依赖
首先需要在pom.xml中添加multiverse-core依赖:
<dependency>
<groupId>org.multiverse</groupId>
<artifactId>multiverse-core</artifactId>
<version>0.7.0</version>
</dependency>
3. Multiverse API核心概念
3.1 STM基础原理
软件事务内存(STM)是从SQL数据库世界移植的概念——每个操作都在满足ACID(原子性、一致性、隔离性、持久性)属性的事务中执行。⚠️ 这里只满足原子性、一致性和隔离性,因为机制运行在内存中。
3.2 核心接口
Multiverse库的核心接口是 TxnObject ——每个事务对象都需要实现它,库提供了多个可直接使用的子类。
3.3 事务执行规则
✅ 需要放在临界区(critical section)的操作(只能由一个线程访问且使用事务对象)必须包装在StmUtils.atomic()
方法中。
✅ 事务成功则提交,新状态对其他线程可见
❌ 出错则不提交,状态保持不变
⚠️ 当两个线程同时修改同一状态时,只有一个能成功提交,另一个将在其事务中重试操作
4. 使用STM实现账户逻辑
4.1 账户类设计
假设我们要用Multiverse的STM实现银行账户逻辑。Account
对象包含:
lastUpdate
:TxnLong
类型的时间戳balance
:TxnInteger
类型的账户余额
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);
}
}
4.2 余额调整方法
实现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("余额不足");
}
});
}
4.3 余额查询方法
获取账户余额也需要原子语义:
public Integer getBalance() {
return balance.atomicGet();
}
5. 账户功能测试
5.1 基础扣款测试
简单扣款操作验证:
@Test
public void givenAccount_whenDecrement_thenShouldReturnProperValue() {
Account a = new Account(10);
a.adjustBy(-5);
assertThat(a.getBalance()).isEqualTo(5);
}
5.2 异常回滚测试
扣款导致余额为负时,事务应回滚:
@Test(expected = IllegalArgumentException.class)
public void givenAccount_whenDecrementTooMuch_thenShouldThrow() {
// given
Account a = new Account(10);
// when
a.adjustBy(-11);
}
5.3 并发冲突测试
两个线程同时扣款(初始余额10):
- 线程1扣款6
- 线程2扣款5
使用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. 账户间转账实现
6.1 转账方法
在Account
类中实现transferTo()
方法:
public void transferTo(Account other, int amount) {
StmUtils.atomic(() -> {
long date = System.currentTimeMillis();
adjustBy(-amount, date);
other.adjustBy(amount, date);
});
}
✅ 所有操作在单一事务中执行,保证原子性 ❌ 转账金额超过余额时,整个事务回滚
6.2 正常转账测试
Account a = new Account(10);
Account b = new Account(10);
a.transferTo(b, 5);
assertThat(a.getBalance()).isEqualTo(5);
assertThat(b.getBalance()).isEqualTo(15);
6.3 异常转账测试
try {
a.transferTo(b, 20);
} catch (IllegalArgumentException e) {
System.out.println("转账失败");
}
assertThat(a.getBalance()).isEqualTo(5);
assertThat(b.getBalance()).isEqualTo(15);
7. STM的死锁安全性
7.1 传统同步的死锁问题
使用标准Java同步机制时,转账操作容易死锁:
- 线程1:锁定账户a → 尝试锁定账户b
- 线程2:锁定账户b → 尝试锁定账户a ⚠️ 结果:程序无限阻塞
7.2 STM的解决方案
STM天然死锁安全!使用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库及其在Java中实现无锁、线程安全的软件事务内存(STM)方案。通过实际案例验证了: ✅ STM的原子性保证 ✅ 事务回滚机制 ✅ 并发冲突处理 ✅ 死锁安全性
所有示例代码可在GitHub项目中获取(Maven项目,可直接导入运行)。对于需要处理复杂共享状态同步的场景,STM提供了一种比传统锁机制更优雅、更安全的解决方案。