编程进阶网编程进阶网
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 1.1String深入理解原理
  • 1.2浮点型数据深入研究
  • 1.3数据装箱和拆箱原理
  • 1.4泛型由来和设计思想
  • 1.5加密和解密设计和原理
  • 2.1面向对象设计思想
  • 2.2抽象类和接口设计
  • 2.3封装和继承设计思想
  • 2.4复用和组合设计思想
  • 2.5对象和引用设计思想
  • 3.1IO流设计思想和原理
  • 3.2为何设计序列化数据
  • 3.3各种拷贝数据比较
  • 3.4高效文件读写的原理
  • 4.1反射性能探索和优化
  • 4.2为何要设计注解思想
  • 4.3动态代理的设计思想
  • 4.4SPI机制设计的思想
  • 4.5异常设计和捕获原理
  • 4.6虚拟机如何处理异常
  • 4.7四种引用设计思想
  • 5.1线程的前世今生探索
  • 5.2线程通信的设计思想
  • 5.3线程监控和Debug设计
  • 5.4线程和JVM之间联系
  • 5.5线程池使用技巧介绍
  • 5.6线程池设计核心原理
  • 5.7线程如何最大优化
  • 6.1多线程并发经典案例
  • 6.2并发安全前世今生
  • 6.3线程安全如何保证
  • 6.4变量的线程安全探索
  • 6.5并发上下文切换原理
  • 6.6理解CAS设计和由来
  • 6.7协程设计思想和原理
  • 6.8事物并发模型解读
  • 6.9并发设计模型研究
  • 6.10并发编程数据一致性
  • 6.11锁问题的定位和修复
  • 6.12多线程如何性能调优
  • 7.1类的加载过程和原理
  • 7.2对象布局设计的原理
  • 7.3双亲委派机制设计思想
  • 7.5代码攻击和安全防护
  • 7.6设计动态生成Java类

6.8事物并发模型解读

目录介绍

  • 01.软件事务并发由来
    • 1.1 天天接触并发案例
    • 1.2 事物并发解决方案
  • 02.用STM实现转账
    • 2.1 先来看一个转账
    • 2.2 转账使用事物实现
    • 2.3 转账使用STM实现
    • 2.4 STM核心的原理
    • 2.5 自己实现STM机制
    • 2.6 STM机制的局限性
  • 03.协程解决并发由来
    • 3.1 协程的由来说明
    • 3.2 协程的应用场景
  • 04.协程解决并发的设计
    • 4.1 协程的整体设计思路
    • 4.2 协程的核心原理

01.软件事务并发由来

1.1 天天接触并发案例

  • 没有机会接触并发编程,实际上天天都在写并发程序,只不过并发相关的问题都被类似MySQL这样的数据库解决呢。
    • 尤其是数据库,在解决并发问题方面,可谓成绩斐然,它的事务机制非常简单易用,能甩 Java 里面的锁、原子类十条街。

1.2 事物并发解决方案

  • 很多编程语言都有从数据库的事务管理中获得灵感,并且总结出了一个新的并发解决方案:
    • 软件事务内存(Software Transactional Memory,简称 STM)。
  • 传统的数据库事务,支持 4 个特性:
    • 原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability),也就是大家常说的 ACID,STM 由于不涉及到持久化,所以只支持 ACI。

02.用 STM 实现转账

2.1 先来看一个转账

  • 并发转账的例子,示例代码如下。
    • 简单地使用 synchronized 将 transfer() 方法变成同步方法并不能解决并发问题,因为还存在死锁问题。
    class UnsafeAccount {
      //余额
      private long balance;
      //构造函数
      public UnsafeAccount(long balance) {
        this.balance = balance;
      }
      //转账
      synchronized void transfer(UnsafeAccount target, long amt){
        if (this.balance > amt) {
          this.balance -= amt;
          target.balance += amt;
        }
      }
    }

2.2 转账使用事物实现

  • 该转账操作若使用数据库事务就会非常简单,如下面的示例代码所示。
    • 如果所有 SQL 都正常执行,则通过 commit() 方法提交事务;如果 SQL 在执行过程中有异常,则通过 rollback() 方法回滚事务。
    • 数据库保证在并发情况下不会有死锁,而且还能保证前面我们说的原子性、一致性、隔离性和持久性,也就是 ACID。
    public class Test{
       public void test() {
            Connection conn = null;
            try{
                //获取数据库连接
                conn = DriverManager.getConnection();
                //设置手动提交事务
                conn.setAutoCommit(false);
                //执行转账SQL
                //......
                //提交事务
                conn.commit();
            } catch (Exception e) {
                //出现异常回滚事务
                conn.rollback();
            }
        }
    }

2.3 转账使用STM实现

  • 用 STM 又该如何实现呢?
    • Java 语言并不支持 STM,不过可以借助第三方的类库来支持,Multiverse就是个不错的选择。
    • 下面的示例代码就是借助 Multiverse 实现了线程安全的转账操作,相比较上面线程不安全的 UnsafeAccount,其改动并不大,仅仅是将余额的类型从 long 变成了 TxnLong ,将转账的操作放到了 atomic(()->{}) 中。
    class Account{
      //余额
      private TxnLong balance;
      //构造函数
      public Account(long balance){
        this.balance = StmUtils.newTxnLong(balance);
      }
      //转账
      public void transfer(Account to, int amt){
        //原子化操作
        atomic(()->{
          if (this.balance.get() > amt) {
            this.balance.decrement(amt);
            to.balance.increment(amt);
          }
        });
      }
    }

2.4 STM核心的原理

  • 一个关键的 atomic() 方法就把并发问题解决了,这个方案看上去比传统的方案的确简单了很多,那它是如何实现的呢?
    • 数据库事务发展了几十年,目前被广泛使用的是 MVCC(全称是 Multi-Version Concurrency Control),也就是多版本并发控制。
  • MVCC 可以简单地理解为数据库事务在开启的时候,会给数据库打一个快照,以后所有的读写都是基于这个快照的。
    • 当提交事务的时候,如果所有读写过的数据在该事务执行期间没有发生过变化,那么就可以提交;如果发生了变化,说明该事务和有其他事务读写的数据冲突了,这个时候是不可以提交的。
    • 为了记录数据是否发生了变化,可以给每条数据增加一个版本号,这样每次成功修改数据都会增加版本号的值。
    • MVCC 的工作原理和 Lock 中提到的乐观锁非常相似。有不少 STM 的实现方案都是基于 MVCC 的,例如知名的 Clojure STM。

2.5 自己实现STM机制

  • 实现STM的思路步骤
    • 第一步:让 Java 中的对象有版本号,数据的每一次修改都对应着一个唯一的版本号。
    • 第二步:对数据的读写操作,一定是在一个事务里面。
    • 第三步:事务对于数据的读写,实现读写的数据的快照。
    • 第四步:模拟实现 Multiverse 中的原子化操作 atomic()。
  • 设计VersionedRef这个类的作用就是将对象 value 包装成带版本号的对象。
    • 数据的每一次修改都对应着一个唯一的版本号,所以不存在仅仅改变 value 或者 version 的情况,用不变性模式就可以很好地解决这个问题,所以 VersionedRef 这个类被我们设计成了不可变的。
    //带版本号的对象引用
    public final class VersionedRef<T> {
      final T value;
      final long version;
      //构造方法
      public VersionedRef(T value, long version) {
        this.value = value;
        this.version = version;
      }
    }
  • 设计TxnRef这个类负责完成事务内的读写操作
    • 读写操作委托给了接口 Txn,Txn 代表的是读写操作所在的当前事务, 内部持有的 curRef 代表的是系统中的最新值。
    //支持事务的引用
    public class TxnRef<T> {
      //当前数据,带版本号
      volatile VersionedRef curRef;
      //构造方法
      public TxnRef(T value) {
        this.curRef = new VersionedRef(value, 0L);
      }
      //获取当前事务中的数据
      public T getValue(Txn txn) {
        return txn.get(this);
      }
      //在当前事务中设置数据
      public void setValue(T value, Txn txn) {
        txn.set(this, value);
      }
    }
  • 事务对于数据的读写,实现读写的数据的快照。
    • STMTxn 是 Txn 最关键的一个实现类,事务内对于数据的读写,都是通过它来完成的。
    • STMTxn 内部有两个 Map:inTxnMap,用于保存当前事务中所有读写的数据的快照;writeMap,用于保存当前事务需要写入的数据。
    • 每个事务都有一个唯一的事务 ID txnId,这个 txnId 是全局递增的。
  • STMTxn 有三个核心方法,分别是读数据的 get() 方法、写数据的 set() 方法和提交事务的 commit() 方法。
    • get() 方法将要读取数据作为快照放入 inTxnMap,同时保证每次读取的数据都是一个版本。
    • set() 方法会将要写入的数据放入 writeMap,但如果写入的数据没被读取过,也会将其放入 inTxnMap。
    • commit() 方法,简化实现,使用了互斥锁,所以事务的提交是串行的。commit() 方法的实现很简单,首先检查 inTxnMap 中的数据是否发生过变化,如果没有发生变化,那么就将 writeMap 中的数据写入(这里的写入其实就是 TxnRef 内部持有的 curRef);如果发生过变化,那么就不能将 writeMap 中的数据写入了。
    //事务接口
    public interface Txn {
      <T> T get(TxnRef<T> ref);
      <T> void set(TxnRef<T> ref, T value);
    }
    //STM事务实现类
    public final class STMTxn implements Txn {
      //事务ID生成器
      private static AtomicLong txnSeq = new AtomicLong(0);
      
      //当前事务所有的相关数据
      private Map<TxnRef, VersionedRef> inTxnMap = new HashMap<>();
      //当前事务所有需要修改的数据
      private Map<TxnRef, Object> writeMap = new HashMap<>();
      //当前事务ID
      private long txnId;
      //构造函数,自动生成当前事务ID
      STMTxn() {
        txnId = txnSeq.incrementAndGet();
      }
    
      //获取当前事务中的数据
      @Override
      public <T> T get(TxnRef<T> ref) {
        //将需要读取的数据,加入inTxnMap
        if (!inTxnMap.containsKey(ref)) {
          inTxnMap.put(ref, ref.curRef);
        }
        return (T) inTxnMap.get(ref).value;
      }
      //在当前事务中修改数据
      @Override
      public <T> void set(TxnRef<T> ref, T value) {
        //将需要修改的数据,加入inTxnMap
        if (!inTxnMap.containsKey(ref)) {
          inTxnMap.put(ref, ref.curRef);
        }
        writeMap.put(ref, value);
      }
      //提交事务
      boolean commit() {
        synchronized (STM.commitLock) {
        //是否校验通过
        boolean isValid = true;
        //校验所有读过的数据是否发生过变化
        for(Map.Entry<TxnRef, VersionedRef> entry : inTxnMap.entrySet()){
          VersionedRef curRef = entry.getKey().curRef;
          VersionedRef readRef = entry.getValue();
          //通过版本号来验证数据是否发生过变化
          if (curRef.version != readRef.version) {
            isValid = false;
            break;
          }
        }
        //如果校验通过,则所有更改生效
        if (isValid) {
          writeMap.forEach((k, v) -> {
            k.curRef = new VersionedRef(v, txnId);
          });
        }
        return isValid;
      }
    }
  • 模拟实现 Multiverse 中的原子化操作 atomic()。
    • atomic() 方法中使用了类似于 CAS 的操作,如果事务提交失败,那么就重新创建一个新的事务,重新执行。
    @FunctionalInterface
    public interface TxnRunnable {
      void run(Txn txn);
    }
    //STM
    public final class STM {
      //私有化构造方法
      private STM() {
      //提交数据需要用到的全局锁  
      static final Object commitLock = new Object();
      //原子化提交方法
      public static void atomic(TxnRunnable action) {
        boolean committed = false;
        //如果没有提交成功,则一直重试
        while (!committed) {
          //创建新的事务
          STMTxn txn = new STMTxn();
          //执行业务逻辑
          action.run(txn);
          //提交事务
          committed = txn.commit();
        }
      }
    }}
  • 自己实现了 STM,并完成了线程安全的转账操作,具体代码如下面所示。
    class Account {
      //余额
      private TxnRef<Integer> balance;
      //构造方法
      public Account(int balance) {
        this.balance = new TxnRef<Integer>(balance);
      }
      //转账操作
      public void transfer(Account target, int amt){
        STM.atomic((txn)->{
          Integer from = balance.getValue(txn);
          balance.setValue(from-amt, txn);
          Integer to = target.balance.getValue(txn);
          target.balance.setValue(to+amt, txn);
        });
      }
    }

2.6 STM机制的局限性

  • STM机制的局限性
    • STM 借鉴的是数据库的经验,数据库虽然复杂,但仅仅存储数据,而编程语言除了有共享变量之外,还会执行各种 I/O 操作,很显然 I/O 操作是很难支持回滚的。
    • STM 不是万能的。目前支持 STM 的编程语言主要是函数式语言,函数式语言里的数据天生具备不可变性,利用这种不可变性实现 STM 相对来说更简单。
贡献者: yangchong211
上一篇
6.7协程设计思想和原理
下一篇
6.9并发设计模型研究