深入理解软件事务内存(STM)及其在JVM中的应用

深入理解软件事务内存(STM)及其在JVM中的应用

booknotes A collection of my book notes on various computer science books booknotes 项目地址: https://gitcode.com/gh_mirrors/bo/booknotes

什么是软件事务内存(STM)

软件事务内存(Software Transactional Memory, STM)是一种并发控制机制,它借鉴了数据库事务的概念来处理共享内存的并发访问问题。在传统的多线程编程中,我们通常使用锁机制来保护共享数据,但锁机制存在诸多问题:容易导致死锁、优先级反转、难以调试等。

STM提供了一种更优雅的解决方案,它将内存操作封装在事务中执行,事务具有ACID特性(原子性、一致性、隔离性),从而简化了并发编程的复杂性。

传统同步机制的问题

传统的同步方式(如Java中的synchronized关键字)虽然能保证线程安全,但会显著降低并发性能。更糟糕的是,如果开发者忘记正确同步代码,JDK不会提供任何警告或错误提示,这可能导致难以发现的并发bug。

对象模型的缺陷

面向对象编程(OOP)将数据和行为封装在一起,这导致我们将对象的身份(identity)与其状态(state)混为一谈。这种设计使得状态可变性成为默认选择,从而带来了各种同步问题。

身份与状态的分离

STM采用了一种不同的编程范式:将对象的身份与其不可变状态分离。这种分离带来了更高的并发性和更少的竞争条件。

身份与状态分离

如上图所示,当Google股票价格更新时,我们不是修改原有价格,而是创建一个新价格记录并更新引用。这种模式只需要快速更改引用指向,而不需要修改原有数据。

为了优化性能,STM使用持久化数据结构来避免对大对象的不必要复制。

STM的工作原理

STM解决了同步的两大核心问题:

  1. 内存屏障问题(可见性问题)
  2. 竞态条件问题

在STM中(以Clojure为例),所有内存访问都发生在事务内部。如果没有冲突,事务可以无锁执行,实现最大并发度;如果检测到冲突,事务管理器会介入解决冲突。

STM的设计原则是:

  • 值是不可变的
  • 身份只能在事务内改变
  • 如果在事务外尝试修改值,会抛出异常
  • 遇到冲突时,事务会自动重试直到成功

开发者需要确保事务代码是幂等的,即多次执行事务会产生相同的结果。这意味着像打印到终端这样的副作用操作不应该放在事务内部。

STM事务特性

STM事务类似于数据库事务,提供以下保证:

  • 原子性(Atomicity):要么所有修改都可见,要么都不可见
  • 一致性(Consistency):事务要么完全执行,要么完全不执行
  • 隔离性(Isolation):事务不会看到其他事务的部分修改

Clojure的STM使用多版本并发控制(MVCC),类似于数据库的乐观锁机制。事务开始时记录状态的时间戳,结束时检查时间戳是否变化,如果变化则回滚事务。

处理写入偏斜异常

写入偏斜(Write Skew)是一种特殊的并发问题:

  1. 两个事务(T1和T2)读取相同的两个值(V1和V2)
  2. T1更新V1,T2更新V2
  3. 两个事务都成功提交,但如果它们的计算依赖于对方读取的值,可能会破坏不变量

在Clojure中,可以使用ensure确保只读的值在事务期间不被外部修改,从而避免写入偏斜问题。

JVM中的STM实现

在JVM生态中,有几种STM实现方式:

  1. Clojure STM:原生支持STM,可以在Java中通过互操作使用
  2. Multiverse STM:提供基于注解的API
  3. Akka STM:支持STM和Actor模型

Akka与Clojure的主要区别在于:如果可变实体没有包装在事务中,它的行为类似于Clojure的原子变量。Akka提供了Java和Scala两种API。

实际应用示例

Java中的Akka STM

public class EnergySource {
    private final long MAXLEVEL = 100;
    final Ref<Long> level = new Ref<Long>(MAXLEVEL);
    // 其他字段和方法...
    
    public boolean useEnergy(final long units) {
        return new Atomic<Boolean>() {
            public Boolean atomically() {
                long currentLevel = level.get();
                if(units > 0 && currentLevel >= units) {
                    level.swap(currentLevel - units);
                    return true;          
                }
                return false;
            }  
        }.execute();
    }
}

Scala中的Akka STM

class EnergySource private() {
    private val MAXLEVEL = 100L
    val level = Ref(MAXLEVEL)
    
    def useEnergy(units : Long) = {
        atomic {
            val currentLevel = level.get
            if(units > 0 && currentLevel >= units) {
                level.swap(currentLevel - units)
                true
            } else false
        }
    }
}

可以看到,Scala版本的代码更加简洁,得益于语言特性对STM的良好支持。

嵌套事务

STM支持嵌套事务,内层事务会被视为外层事务的一部分执行。例如在转账场景中,存款和取款操作可以各自作为事务,同时整个转账过程也是一个事务。

STM的局限性

虽然STM简化了并发编程,但它也有适用场景:

  • 非常适合典型的Web应用场景(频繁读写非共享数据,偶尔读写共享数据)
  • 不适合写操作非常频繁的场景
  • 相比Java传统并发模型,STM更简单且不易出错

总结

软件事务内存提供了一种全新的并发编程范式,通过借鉴数据库事务的概念,大大简化了共享内存的并发控制。它将身份与状态分离,使用不可变值和事务性引用来管理并发访问,避免了传统锁机制的诸多问题。

在JVM生态中,Clojure原生支持STM,而Akka/Multiverse为Java和Scala提供了STM实现。虽然STM不是万能的,但在适当的场景下,它能显著提高开发效率并减少并发错误。

对于正在探索更好并发解决方案的Java开发者来说,STM值得深入了解和实践。

booknotes A collection of my book notes on various computer science books booknotes 项目地址: https://gitcode.com/gh_mirrors/bo/booknotes

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

咎竹峻Karen

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值