查看原文
其他

Java为何能将读与写封装为一个原子操作

易哥 架构研究所 2022-09-08
文章前记

        程序员工作久了便可能整日忙碌于“增删改查”中,迷失方向,毫无进步。

        该公众号致力于分享软件开发相关的原创干货,助你完成从程序员到架构师的进阶之路!

        努力!做一个NB的Coder!





1背景



在编程中,我们知道如果对一个变量读取然后写入这并不是一个原子操作,例如我们常用的i++操作,其实可以简单划分为以下三步:





以上三步操作时独立的,因此并不是原子化的。如果i变量在第1步和第3步之间被其他线程更改则会引发意向不到的结果。例如线程1对i=5进行i++操作时,线程2对变量i进行了写入操作,则还会发生混乱。如图所示:







此时我们看到,因为i++操作不是原子的,中间受到线程2的影响,使得i++的操作是错误的。


如果是原子化操作,则i++操作要么发生在线程2的操作之前,最终得到i=3;要么发生在线程2操作之后,得到i=4。而不能出现i=6这样的结果。


其实i++就是一个最简单的汇聚了读取和写入的操作,因此读取并写入操作并不是原子化的。可是,Java中确实能够将读取并写入操作封装为一个原子化操作。那它是怎样实现的呢?


我们先介绍一种粗暴的实现:使用同步块实现原子化。






2使用同步块实现原子化


为了能实现i++操作的原子化,则最容易想到的就是使用同步块:



但这显然大材小用,将整个操作放置在同步块中,虽然借助同步块实现了原子化,但因为同步块的操作要进行锁的分配等,使得程序的整体效率受到了影响。


而我们知道Java中AtomicInteger类可以直接将读和写封装在一个原子操作中实现原子化的i++等操作,那它们是怎么实现的呢?我们接下来进行探讨。






3硬件CAS操作


Java中AtomicInteger类便可以将读和写封装在一个原子操作中。那它是怎么实现的呢?


它使用的是CAS操作。CAS是compare and swap的缩写,中文可以翻译成:比较并交换。CAS操作来源于底层硬件领域。因为CAS能够极大地提高并发效率,因此在硬件设计领域,CAS这种操作就是存在的。例如在intel的CPU中,使用cmpxchg指令就可以实现CAS操作。我们详细介绍下CAS操作的原理:


CAS操作一共包含三个操作数:内存位置(V)、预期原值(A)和新值(B)。具体操作过程如下:


  1. 首先,返回V位置的原值R

    • 如果内存位置的值R与预期原值A相匹配,那么处理器会自动将该位置值更新为新值

    • 否则,处理器不做任何操作


无论结果如何,该操作总能拿到V处的原值R,如果R等于A,则说明V处的值已经被更新为了B;否则说明更新失败,V处的值依旧是R。因此,经过该操作后,可以知道CAS操作是否成功。


当然,还有一种CAS实现是直接返回操作是否成功的结果,即:如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值,并返回操作成功;否则,返回操作失败。


但是这与返回V位置原值的方式大同小异,而且返回原值R的实现方式不仅能获得操作是否成功的结果,还能顺便获得R值,因此更为常用。






4Java中的CAS操作


在Java初期,java是无法直接利用这些硬件操作来提升并发性能的,直到后来Java本地方法(JNI)的出现,使得Java直接调用本地方法称为可能,在Doug Lea提供的cucurenct包得以实现。


在Java中如何实现呢?其实知道了CAS操作的原理,一下就变得简单起来。我们还是以i++操作为例,介绍CAS整个过程:


  1. 读取内存位置V的原值R


  2. 因为进行的是i++操作,因此CAS操作的预期原值A=R、新值B=R+1,进行CAS。


  3. 获取V的原值R


    • 如果R等于A,更改成功,操作结束


    • 如果R不等于A,则没有更改。但是获取当前时刻V的原值,将R重新设置为该值。则预期原值A=R、新值B=R+1,然后在继续进行CVS。


空口无凭,我们看一下AtomicInteger中i++操作的实现方法getAndIncrement():



而getAndAddInt()方法如下:



我们继续追踪到compareAndSwapInt()方法,分析该方法是一个本地方法。



稍加需要注意的是,可能由于其他原因,这里的compareAndSet采用的是返回操作是否成功的实现方式,而不是返回当前内存位置原值R的方式。因此在每一次进行CAS操作时,都需要先调用getIntVolatile(var1, var2)方法获取一次当前的变量值作为预期原值A。


因此,getAndIncrement()的具体操作就是:


  1. 获取当前值,在此基础上+1作为目标值


  2. 进行CAS操作


  3. 如果不成功,说明值不是最新的。重新获取值,不断再试。


如此一来,我们就知道了Java中是如何将读取和写入这两个操作封装成一个原子操作的。





5总结


CAS的思想本质上就是乐观锁思想。即:假设情况都是乐观的,那么当前内存位置V的值是预期原值A,如果这样,请把内存位置V的值设置为新值B;否则,乐观的情况不成立,什么也别做。


乐观锁的思想减少了使用同步块等悲观锁的开销,但是本质是基于乐观锁思想的,因此肯定存在一些问题。


对与CAS引发的问题与解决办法,我们将会在下次文章中详细介绍。




End




扫码关注不错过每期的架构师干货!



您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存