Java并发编程实战~软件事务内存

很多同学反馈说,工作了挺长时间但是没有机会接触并发编程,实际上我们天天都在写并发程序,只不过并发相关的问题都被类似 Tomcat 这样的 Web 服务器以及 MySQL 这样的数据库解决了。尤其是数据库,在解决并发问题方面,可谓成绩斐然,它的事务机制非常简单易用,能甩 Java 里面的锁、原子类十条街。技术无边界,很显然要借鉴一下。
其实很多编程语言都有从数据库的事务管理中获得灵感,并且总结出了一个新的并发解决方案:软件事务内存(Software Transactional Memory,简称 STM)。传统的数据库事务,支持 4 个特性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability),也就是大家常说的 ACID,STM 由于不涉及到持久化,所以只支持 ACI。

STM 的使用很简单,下面我们以经典的转账操作为例,看看用 STM 该如何实现。

用 STM 实现转账

我们曾经在《一不小心就死锁了》这篇文章中,讲到了并发转账的例子,示例代码如下。简单地使用 synchronized 将 transfer() 方法变成同步方法并不能解决并发问题,因为还存在死锁问题。

class UnsafeAccount {//余额private long balance;//构造函数public UnsafeAccount(long balance) {this.balance = balance;}//转账void transfer(UnsafeAccount target, long amt){if (this.balance > amt) {this.balance -= amt;target.balance += amt;}}
}

该转账操作若使用数据库事务就会非常简单,如下面的示例代码所示。如果所有 SQL 都正常执行,则通过 commit() 方法提交事务;如果 SQL 在执行过程中有异常,则通过 rollback() 方法回滚事务。数据库保证在并发情况下不会有死锁,而且还能保证前面我们说的原子性、一致性、隔离性和持久性,也就是 ACID。

Connection conn = null;
try{//获取数据库连接conn = DriverManager.getConnection();//设置手动提交事务conn.setAutoCommit(false);//执行转账SQL......//提交事务conn.commit();
} catch (Exception e) {//出现异常回滚事务conn.rollback();
}

那如果用 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);}});}
}

一个关键的 atomic() 方法就把并发问题解决了,这个方案看上去比传统的方案的确简单了很多,那它是如何实现的呢?数据库事务发展了几十年了,目前被广泛使用的是 MVCC(全称是 Multi-Version Concurrency Control),也就是多版本并发控制。

MVCC 可以简单地理解为数据库事务在开启的时候,会给数据库打一个快照,以后所有的读写都是基于这个快照的。当提交事务的时候,如果所有读写过的数据在该事务执行期间没有发生过变化,那么就可以提交;如果发生了变化,说明该事务和有其他事务读写的数据冲突了,这个时候是不可以提交的。

为了记录数据是否发生了变化,可以给每条数据增加一个版本号,这样每次成功修改数据都会增加版本号的值。MVCC 的工作原理和我们曾经在《StampedLock》中提到的乐观锁非常相似。有不少 STM 的实现方案都是基于 MVCC 的,例如知名的 Clojure STM。
下面我们就用最简单的代码基于 MVCC 实现一个简版的 STM,这样你会对 STM 以及 MVCC 的工作原理有更深入的认识。

自己实现 STM

我们首先要做的,就是让 Java 中的对象有版本号,在下面的示例代码中,VersionedRef 这个类的作用就是将对象 value 包装成带版本号的对象。按照 MVCC 理论,数据的每一次修改都对应着一个唯一的版本号,所以不存在仅仅改变 value 或者 version 的情况,用不变性模式就可以很好地解决这个问题,所以 VersionedRef 这个类被我们设计成了不可变的。

所有对数据的读写操作,一定是在一个事务里面,TxnRef 这个类负责完成事务内的读写操作,读写操作委托给了接口 Txn,Txn 代表的是读写操作所在的当前事务, 内部持有的 curRef 代表的是系统中的最新值。

//带版本号的对象引用
public final class VersionedRef<T> {final T value;final long version;//构造方法public VersionedRef(T value, long version) {this.value = value;this.version = version;}
}
//支持事务的引用
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<>();//当前事务IDprivate long txnId;//构造函数,自动生成当前事务IDSTMTxn() {txnId = txnSeq.incrementAndGet();}//获取当前事务中的数据@Overridepublic <T> T get(TxnRef<T> ref) {//将需要读取的数据,加入inTxnMapif (!inTxnMap.containsKey(ref)) {inTxnMap.put(ref, ref.curRef);}return (T) inTxnMap.get(ref).value;}//在当前事务中修改数据@Overridepublic <T> void set(TxnRef<T> ref, T value) {//将需要修改的数据,加入inTxnMapif (!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,并完成了线程安全的转账操作,使用方法和 Multiverse 差不多,这里就不赘述了,具体代码如下面所示。

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);});}
}

总结

STM 借鉴的是数据库的经验,数据库虽然复杂,但仅仅存储数据,而编程语言除了有共享变量之外,还会执行各种 I/O 操作,很显然 I/O 操作是很难支持回滚的。所以,STM 也不是万能的。目前支持 STM 的编程语言主要是函数式语言,函数式语言里的数据天生具备不可变性,利用这种不可变性实现 STM 相对来说更简单。

另外,需要说明的是,文中的“自己实现 STM”部分我参考了Software Transactional Memory in Scala这篇博文以及一个 GitHub 项目,目前还很粗糙,并不是一个完备的 MVCC。如果你对这方面感兴趣,可以参考Improving the STM: Multi-Version Concurrency Control 这篇博文,里面讲到了如何优化,你可以尝试学习下。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/496633.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python list去重时间复杂度_List集合去重的一种方法 z

需要对一个List集合去重&#xff0c;情况是该集合中会出现多个Name属性值相同的&#xff0c;但是其他属性值不同的数据。在这种情况下&#xff0c;需求要只保留其中一个就好。我觉得遍历和HashSet都不是我想要的&#xff0c;便采用了一下方式定义Compare类&#xff0c;继承IEqu…

对于Office Live平台的思考

刚接触计算机编程的时候&#xff0c;脑子里想法比肚子里的墨水多得多&#xff0c;那时候想通过网络成立一个游戏开发团队&#xff0c;将不少人都很喜欢的一款FC游戏“重装机兵”&#xff08;Metal Max&#xff09;移植到电脑上来。当时的想法很激进也很宏大&#xff0c;我想的不…

中国学者用人工光感受器助失明小鼠复明

来源&#xff1a;《自然—通讯》中国研究人员在英国《自然通讯》杂志上发表报告说&#xff0c;他们通过在失明小鼠眼底植入一种新研发的人工光感受器&#xff0c;让它们的视觉得以恢复。如果这种技术发展成熟&#xff0c;未来或许能帮助因黄斑变性等疾病而视力下降或失明的患者…

PoW算法

文章目录前言一、PoW——工作量证明二、go语言简单案例前言 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 一、PoW——工作量证明 ⚫ Proof-of-Work 简称 PoW&#xff0c;即为工作量证明 ⚫ 通过计算一个数值&#xff0c;使得拼揍上交易数据后内容的…

systemctl 命令完全指南

From:https://linux.cn/article-5926-1.html systemctl命令是系统服务管理器指令&#xff0c;它实际上将 service 和 chkconfig 这两个命令组合到一起。 任务 旧指令 新指令 使某服务自动启动 chkconfig --level 3 httpd on systemctl enable httpd.service 使某服务不自…

boundcolumn 根据值进行判断_Excel使用函数进行条件判断的方法步骤

Excel中的函数具体该如何进行判断数据的条件是否达到要求呢?下面是学习关于excel使用函数进行条件判断的教程&#xff0c;希望阅读过后对你有所启发! excel使用函数进行条件判断的教程 函数条件判断步骤1&#xff1a;如何计算成绩是否合格 函数条件判断步骤2&#xff1a;选中要…

城市大脑不仅是AI系统,更是结合人类智慧的混合智能巨系统

作者&#xff1a;刘锋 《互联网进化论》作者从2015年开始&#xff0c;智慧城市的类脑化进程不断加速&#xff0c;包括城市大脑&#xff0c;城市云脑&#xff0c;城市神经系统&#xff0c;智慧城市脑&#xff0c;交通大脑等概念不断涌现&#xff0c;人工智能成为当前科技热点的今…

转载:ListBox的SelectedValue和SelectedItem的区别

转载&#xff1a;ListBox的SelectedValue和SelectedItem的区别 原文&#xff1a;http://www.beacosta.com/blog/?p9What is the difference between SelectedValue and SelectedItem? When they are used by themselves, these two properties are very similar. The need fo…

区块链简单实现

前言 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 一、区块链简单实现 package Blockimport ("crypto/sha256""encoding/hex""fmt""strconv""strings""time" )type Block struct{Pre…

charts漏斗图表_用echarts写的转换率图表(漏斗图 + 象形柱图)

平时用图表可能都是常规的折线图&#xff0c;柱状图&#xff0c;饼图这些基本的。下面的是一个漏斗图 象形柱图写出来的图表。完成后的图表&#xff0c;关键是右半边漏斗图很简单&#xff0c;只讲下象形柱图里的几个关键点(因为这里的用法有点特别&#xff0c;不是文档里的常规…

Java并发编程实战~协程

Golang 是一门号称从语言层面支持并发的编程语言&#xff0c;支持并发是 Golang 一个非常重要的特性。在上一篇文章《44 | 协程&#xff1a;更轻量级的线程》中我们介绍过&#xff0c;Golang 支持协程&#xff0c;协程可以类比 Java 中的线程&#xff0c;解决并发问题的难点就在…

SQL的经典语句(太全了)

关键字: sql语句 查询表内容 SELECT 表名case when a.colorder1 then d.name else end, 表说明case when a.colorder1 then isnull(f.value,) else end, 字段序号a.colorder, 字段名a.name, 标识case when COLUMNPROPERTY( a.id,a.name,IsIdentity)1 then √else end, 主键…

上帝的指纹——分形与混沌

来源&#xff1a;王东明科学网博客云朵不是球形的&#xff0c;山峦不是锥形的&#xff0c;海岸线不是圆形的&#xff0c;树皮不是光滑的&#xff0c;闪电也不是一条直线。——分形几何学之父Benoit Mandelbrot话说在一个世纪以前&#xff0c;数学领域相继出现了一些数学鬼怪&am…

PoS算法

文章目录前言一、PoS——权益证明二、go语言简单实现前言 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 一、PoS——权益证明 PoS 是什么 ⚫ PoS&#xff08;Proof of Stake&#xff09;译为权益证明&#xff0c;是一种在公链中的共识算法&#xf…

mysql 内存 256m_解决mySQL占用内存超大问题

为了装mysql环境测试&#xff0c;装上后发现启动后mysql占用了很大的虚拟内存&#xff0c;达8百多兆。网上搜索了一下&#xff0c;得到高人指点my.ini。再也没见再详细的了..只好打开my.ini逐行的啃&#xff0c;虽然英文差了点&#xff0c;不过多少M还是看得明的^-^更改后如下&…

scrapy 教程

------------------------------------------------------------------------------------------ scrapy 中文文档 和 scrapy 英文文档参照看。因为中文文档比较老&#xff0c;英文文档是最新的。 scrapy 英文文档&#xff1a;https://doc.scrapy.org/en/latest scrapy 中文文档…

Java加密与解密的艺术~DigestInputStream

import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.security.DigestInputStream; import java.security.MessageDigest;/*** TODO 在此写上类的相关说明.<br>* author gqltt<br>* version 1.0.0 2021年11月29日<b…

智慧停车产业链市场全透析

来源&#xff1a;慧天地最近&#xff0c;Goodwin调查发现&#xff0c;在城市地区&#xff0c;接近30%的交通拥堵源自于司机寻找停车位。据称&#xff0c;到2020年&#xff0c;将会有20亿的汽车在公路上跑着&#xff0c;这就意味着&#xff0c;届时汽车的数量将比目前多了7.7亿辆…

语音合成与识别技术在C#中的应用 (转 )

语音合成与识别技术在C#中的应用 (转 &#xff09; 我们要想实现中文发音或中文语音识别&#xff0c;必需先安装微软的Speech Application SDK&#xff08;SASDK&#xff09;&#xff0c;它的最新版本是 SAPI 5.1 他能够识别中、日、英三种语言&#xff0c;你可以在这里下载&am…

left join 和join区别_sleep、yield、join方法简介与用法 sleep与wait区别 多线程中篇

Object中的wait、notify、notifyAll&#xff0c;可以用于线程间的通信&#xff0c;核心原理为借助于监视器的入口集与等待集逻辑通过这三个方法完成线程在指定锁&#xff08;监视器&#xff09;上的等待与唤醒&#xff0c;这三个方法是以锁&#xff08;监视器&#xff09;为中心…