JUC-AtomicInteger分析

413 阅读6分钟

这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战

JUC-AtomicInteger分析

原子类型就是一种无锁的、线程安全的、包含基本数据类型和引用类型的,很好的多线程并发数据安全解决方案。

我们知道对于诸如x=x+1这样的表达式,虽然看起来只有一行代码,实现对x加一的操作,但是实际上底层实现是拆分成了三个操作:a.读取x的值。b.对x加一。c.将新值写入x。这三个操作独立起来是原子性操作,但是串联起来,却未必。

所谓的原子操作,就是指一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

所以为了确保这个操作的线程安全,你可能会给x加上volatile声明,但是,volatile修饰的作用是,确保x的修改在多线程之间立即可见,以及禁止Java的编译器对相关字节码指令进行重排序优化。并不能保证,多个线程同时读写x的原子性和正确性。

一个可行的方式是,在诸如x=x+1这种非原子操作,存在多线程并发的场景的时候,将相关操作用synchronize块包裹,这样当一个线程获取到锁的时候,其它线程必须等待当前线程执行完锁定的代码,然后才能去尝试获取锁,以此保证,x=x+1操作在同一时间,只会有一个线程在操作。

如果觉得synchronize在这里太重了,我们还可以考虑通过Lock加锁的方式,可能可以获得比synchronize更好的性能表现。

但是我们仅仅只是去修改一些基本类型的值,这个操作无论加上synchronize还是Lock锁操作,性能上并不划算,而更高效的方式就是使用原子类型进行操作。

AtomicInteger介绍:

AtomicInteger位于JUC包下,和普通的Integer一样继承自Number类,但是并不完全对等Integer,这个类知识对Integer的原子性操作的一个扩充。

下面是一些基本API:

int getAndIncrement()原子性的i++,返回的是自增前的值
int incrementAndGet()原子性的++i,返回的是自增后的值
getAndDecrement()、decrementAndGet()原子性的i--和--i
boolean compareAndSet(int expect, int update)原子性的x=y。返回boolean表示是否更新成功,只有在当前的值和expect值一致的时候,才会进行更新,否则失败。
int getAndAdd(int delta)、int addAndGet(int delta)原子性地更新AtomicInteger 的value值,更新后的value为value和delta之和,方法的返回值情况类似上面。
void set(int newValue)、void set(int newValue)AtomicInteger内部有一个volatile修饰的value字段,set方法修改被volatile关键字修饰的value值会被强制刷新到主内存中,从而立即被其他线程看到,但是volatile本身的内存屏障保证线程可见也是有开销的,比如,在单线程中对AtomicInteger的value进行修改时没有必要保留内存屏障,而value又是被volatile关键字修饰的,而lazySet方法的作用就是用于value的更新不需要考虑线程立即可见的场景。

核心API分析:

如果有去查看AtomicInteger的源码,我们会接触到两个概念,CAS算法和自旋。

CAS:

CAS全称compara and set(也有说是swap)。一个CAS操作包含三个值,内存值V,修改时的预期值A,要修改的新值B,执行修改的时候,只有当V和A一致的时候,才会将B更新到内存值V上,否则什么都不做。

对于AtomicInteger,compareAndSet调用的是Unsafe类的compareAndSetInt方法。

U.compareAndSetInt(this, VALUE, expectedValue, newValue);

该方法四个入参,this表示操作的对象,即当前的AtomicInteger对象,VALUE表示的是AI对象内部的value字段相对对象的内存地址偏移量,expectedV和newV即上面CAS提到的A和B。

Unsafe的compareAndSetInt方法我们需要在jdk源码中查看:

hg.openjdk.java.net/jdk10/jdk10…

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSetInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)) {
  // oop本身是一个JVM底层描述对象的数据接口,这里可以理解为是获取AtomicInteger对象实例
  oop p = JNIHandles::resolve(obj);
  // 根据offet获取到value字段的地址
  jint* addr = (jint *)index_oop_from_field_offset_long(p, offset);
  // 通过Atomic::cmpxchg操作实现CAS
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
} UNSAFE_END

不同版本JDK可能略有差异,但做的事儿都是一样的。

而Atomic::cmpxchg就是在不同CPU架构下,调用对应架构的汇编代码,

inline T Atomic::PlatformCmpxchg<4>::operator()(T exchange_value,
                                                T volatile* dest,
                                                T compare_value,
                                                cmpxchg_memory_order /* order */) const {
  STATIC_ASSERT(4 == sizeof(T));
  __asm__ volatile (  "lock cmpxchgl %1,(%3)"
                    : "=a" (exchange_value)
                    : "r" (exchange_value), "a" (compare_value), "r" (dest)
                    : "cc", "memory");
  return exchange_value;
}

通过__asm__内联汇编,volatile禁用编译器优化。执行了lock cmpxchgl指令。cmpxchgl是用于比较并交换操作数的指令,但是这个指令并不是原子性的,所以要加上lock前缀(CPU保证被其修饰的指令的原子性),其通过总线锁或缓存一致性,保证被修饰指令操作的数据一致性。

  1. 总线锁:通过锁定系统总线,阻塞其它处理器的请求。

  2. 缓存一致性:当处理器去访问缓存在其他处理器中的数据时,不能得到错误的数据。如果数据被修改,那么其他处理器也必须得到修改后的数据。 

自旋:

诸如getAndAddInt这些API,是一定要修改成功的,可能容易误以为,我们只需要写出类似下面的代码就一定可以成功:

ai.comparaAndSet(ai.get(), 10)

但是实际上,AtomicInteger和synchronize或者Lock并不属于同一套机制,前者是在CPU层面,保证值修改CPU指令的线程安全,而后者是通过锁机制,在进行值修改的时候直接排斥其它线程操作。所以也就存在线程A调用完ai.get()获取到值之后,线程B实际上已经完成了对ai新值的更新,这样,在这里就会导致ai.get这和预期值和内存中的值不一致,产生失败。

但是对于incrementAndGet这种API,其操作是一定要成功的。incrementAndGet执行的是Unsafe的getAndAddInt方法:

public final int incrementAndGet() {
    return U.getAndAddInt(this, VALUE, 1) + 1;
}

public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);
    } while (!weakCompareAndSetInt(o, offset, v, v + delta));
    return v;
}

内部通过getIntVolatile获取修改之前的value,然后死循环weakCompareAndSetInt直到修改成功。而weakCompareAndSetInt实际上就是调用上面分析的compareAndSetInt。

所以在并发的场景下,incrementAndGet这种API,得到的并不一定是调用处的V+1,可能是更大的值。

用一个图来解释这个过程: