Java为何能将读与写封装为一个原子操作
保留所有版权,请引用而不是转载本文(原文地址 https://yeecode.top/blog/20/ )。
在编程中,我们知道如果对一个变量读取然后写入这并不是一个原子操作,例如我们常用的i++操作,其实可以简单划分为以下三步:
1 读入i变量
2 对i变量进行+1操作
3 将新的i值写入
【1-1】
以上三步操作时独立的,因此并不是原子化的。如果i变量在第1步和第3步之间被其他线程更改则会引发意向不到的结果。例如线程1对i=5进行i++操作时,线程2对变量i进行了写入操作,则还会发生混乱。如图所示:
线程1 读入i变量,i=5
线程2 对i写入新的值:i=3
线程1 对变量i进行+1操作,i=6
线程1 对i进行写入:i=6
【1-2】 此时我们看到,因为i++操作不是原子的,中间受到线程2的影响,使得i++的操作是错误的。如果是原子化操作,则i++操作要么发生在线程2的操作之前,最终得到i=3;要么发生在线程2操作之后,得到i=4。而不能出现i=6这样的结果。
其实i++就是一个最简单的汇聚了读取和写入的操作,因此读取并写入操作并不是原子化的。可是,Java中确实能够将读取并写入操作封装为一个原子化操作。那它是怎样实现的呢?
我们先介绍一种粗暴的实现:使用同步块实现原子化。
使用同步块实现原子化
为了能实现i++操作的原子化,则最容易想到的就是使用同步块:
synchronized int selfIncrement(int value){
return value++;
}
但这显然大材小用,将整个操作放置在同步块中,虽然借助同步块实现了原子化,但因为同步块的操作要进行锁的分配等,使得程序的整体效率受到了影响。
而我们知道Java中AtomicInteger类可以直接将读和写封装在一个原子操作中实现原子化的i++等操作,那它们是怎么实现的呢?我们接下来进行探讨。
硬件CAS操作
Java中AtomicInteger类便可以将读和写封装在一个原子操作中。那它是怎么实现的呢?
它使用的是CAS操作。CAS是compare and swap的缩写,中文可以翻译成:比较并交换。CAS操作来源于底层硬件领域。因为CAS能够极大地提高并发效率,因此在硬件设计领域,CAS这种操作就是存在的。例如在intel的CPU中,使用cmpxchg指令就可以实现CAS操作。我们详细介绍下CAS操作的原理:
CAS操作一共包含三个操作数:内存位置(V)、预期原值(A)和新值(B)。具体操作过程如下:
- 首先,返回V位置的原值R
- 如果内存位置的值R与预期原值A相匹配,那么处理器会自动将该位置值更新为新值
- 否则,处理器不做任何操作
无论结果如何,该操作总能拿到V处的原值R,如果R等于A,则说明V处的值已经被更新为了B;否则说明更新失败,V处的值依旧是R。因此,经过该操作后,可以知道CAS操作是否成功。
当然,还有一种CAS实现是直接返回操作是否成功的结果,即:如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值,并返回操作成功;否则,返回操作失败。但是这与返回V位置原值的方式大同小异,而且返回原值R的实现方式不仅能获得操作是否成功的结果,还能顺便获得R值,因此更为常用。
Java中的CAS操作
在Java初期,java是无法直接利用这些硬件操作来提升并发性能的,直到后来Java本地方法(JNI)的出现,使得Java直接调用本地方法称为可能,在Doug Lea提供的cucurenct包得以实现。
在Java中如何实现呢?其实知道了CAS操作的原理,一下就变得简单起来。我们还是以i++操作为例,介绍CAS整个过程:
- 读取内存位置V的原值R
- 因为进行的是i++操作,因此CAS操作的预期原值A=R、新值B=R+1,进行CAS。
- 获取V的原值R
- 如果R等于A,更改成功,操作结束
- 如果R不等于A,则没有更改。但是获取当前时刻V的原值,将R重新设置为该值。则预期原值A=R、新值B=R+1,然后在继续进行CVS。
空口无凭,我们看一下AtomicInteger中i++操作的实现方法getAndIncrement():
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
而getAndAddInt()方法如下:
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
我们继续追踪到compareAndSwapInt()方法,分析该方法是一个本地方法。
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
稍加需要注意的是,可能由于其他原因,这里的compareAndSet采用的是返回操作是否成功的实现方式,而不是返回当前内存位置原值R的方式。因此在每一次进行CAS操作时,都需要先调用getIntVolatile(var1, var2)方法获取一次当前的变量值作为预期原值A。
因此,getAndIncrement()的具体操作就是: 1. 获取当前值,在此基础上+1作为目标值 2. 进行CAS操作 3. 如果不成功,说明值不是最新的。重新获取值,不断再试。
如此一来,我们就知道了Java中是如何将读取和写入这两个操作封装成一个原子操作的。
总结
CAS的思想本质上就是乐观锁思想。即:假设情况都是乐观的,那么当前内存位置V的值是预期原值A,如果这样,请把内存位置V的值设置为新值B;否则,乐观的情况不成立,什么也别做。
乐观锁的思想减少了使用同步块等悲观锁的开销,但是本质是基于乐观锁思想的,因此肯定存在一些问题。
可以访问个人知乎阅读更多文章:易哥(https://www.zhihu.com/people/yeecode),欢迎关注。