前言
今天的笔记我们继续读《Java并发编程的艺术-第二章》,来了解一下原子操作以及Java中如何实现原子操作。
概念
原子(atomic)本意是“不能被进一步分割的最小粒子”,而原子操作(atomic operation)意为“不可被中断的一个或一系列操作”。
处理器实现原子操作
处理器会保证基本内存操作的原子性。处理器保证从系统内存中读取或者写入一个字节是原子的,意思是当一个处理器读取一个字节时,其他处理器不能访问这个字节的内存地址。最新的处理器能自动保证单处理器进行16/32/64位的操作是原子的,并且提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性。
使用总线保证原子性
如果有多个处理器同时对共享变量进行操作,那么共享变量就会被多个处理器同时操作,这样的话,读改写操作就不是原子的。比如i=1,i++,两个处理器同时进行操作,最后的结果,可能是3,也可能是3.原因可能是多个处理器同时从各自的缓存中读取变量i,分别进行加1操作,然后分别写入系统内存。处理器使用总线锁来解决这个问题。当处理器发出LOCK#信号时,其他处理器的请求会被阻塞主,该处理器可以独占共享内存。
使用缓存锁定来保证原子性
锁总线开销还是很大的,锁住了CPU和内存之间的通信。因为频繁使用的内存会缓存在处理器的L1、L2、L3高速缓存中,原子操作可以在缓存内部完成,同时通过缓存一致性协议,当A处理器修改缓存中的i时,其他处理器不能同时缓存i,即会使得其他处理器中对于共享变量的缓存失效。这段还不是特别明白,感觉得重新翻一下操作系统,有知道的网友可以留言补充一下。
Java实现原子操作的方式
Java可以使用锁,实现一段代码的原子操作。但这样开销比较大,会引起频繁的上下文切换。另外一种方式就是使用CAS操作(比较交换)。CAS算法的过程是比较简单的。它会包含三个参数(V,E,N))。V表示要更新的变量,E表示预期值,N值。当且仅当V等于E值时,才会将V的值设为N,如果V值和E值不同,说明已经有其他线程做了更新,则当前线程什么都不做。当多个线程同时使用CAS对变量进行操作时,只有一个会胜出并成功更新,其余会失败。失败的线程不会被挂起。Java中对于基本类型的包装类都有对应的原子操作实现,比如AtomicBoolean,AtomicInteger等。如果拿AtomicInteger为例子,其中的incrementAndGet的实现如下所示,是直接调用了Unsafe类的方法:
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;
}
其中var1是传入对象的引用,var2是字段到对象头部的偏移量,方便快速定位,var5是当前值,var5+var4就是期望值,var4传入的是1。如果是引用类型的话,可以使用AtomicReference。CAS操作虽好,但它会遇到ABA问题,即一个变量先是A,后来变成了B,在比较时又变回了A,但CAS操作无法感知到这种情况,如果说我们是否可以修改当前值,不仅取决于当前值,还取决于它的变化,那么原有的CAS操作就无能为力了,因为它感知不到。贴心的JDK为我们提供了AtomicStampedReference,它在对象内部维护了时间戳,当更新数据时,不仅要更新数据,还要更新时间戳。当AtomicStampedReference设置对象值时,对象值以及时间戳都必须满足期望值,写入才会成功。如果是数组类型的话,JDK提供了AtomicIntegerArray等数组类型的原子类。