这是我参与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保证被其修饰的指令的原子性),其通过总线锁或缓存一致性,保证被修饰指令操作的数据一致性。
-
总线锁:通过锁定系统总线,阻塞其它处理器的请求。
-
缓存一致性:当处理器去访问缓存在其他处理器中的数据时,不能得到错误的数据。如果数据被修改,那么其他处理器也必须得到修改后的数据。
自旋:
诸如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,可能是更大的值。
用一个图来解释这个过程: