在高并发情况下,同步问题是一个不可轻视的问题,有时我们可以使用volatile关键字或者synchronized来实现同步,但是这两种方式都有各自的缺点,volatile关键字只保证了线程之间的可见性,却无法保证操作的原子性,而synchronized由于其是重量级锁,带来的效率问题也很让人头疼。
引入:
首先我们看一个特别简单的代码:
public class Case {
public volatile int n;
public void add() {
n++;
}
}
字节码如下:
public void add();
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field n:I
5: iconst_1
6: iadd
7: putfield #2 // Field n:I
10: return
我们可以看到,n++被拆成了三个步骤:
- 执行
getfield
拿到原始n; - 执行
iadd
进行加1操作; - 执行
putfield
写把累加后的值写回n;
所以这就有了关于volatile关键字的问题:通过volatile修饰的变量可以保证线程之间的可见性,但并不能保证这3个指令的原子执行,在多线程并发执行下,无法做到线程安全,得到正确的结果。如果为了保证原子性而采用了synchronized关键字,势必会造成性能低下的问题。
JAVA原子类:
除了低性能的加锁方案,我们还可以使用JDK自带的CAS方案,在CAS中,比较和替换是一组原子操作,不会被外部打断,且在性能上更占有优势。
在java.util.concurrent.atomic包下有很多类:
其中后四个我们不讨论。
形如AtomicXXX的类我们称之为原子类,顾名思义,它可以完成简单的原子操作。
我们以AtomicInteger为例,看看是如何实现原子操作的(此源码未包含方法定义):
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
}
- Unsafe,是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。
- 变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。
- 变量value用volatile修饰,保证了多线程之间的内存可见性。
下面看看AtomicInteger在并发环境下如何实现累加的操作:
/**
* Atomically sets to the given value and returns the old value.
*
* @param newValue the new value
* @return the previous value
*/
public final int getAndSet(int newValue) {
return unsafe.getAndSetInt(this, valueOffset, newValue);
}
下面是Unsafe类中的getAndSetInt方法:
public final int getAndSetInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var4));
return var5;
}
假设线程A和线程B同时执行getAndAdd操作(分别跑在不同CPU上):
- AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value为3,根据Java内存模型,线程A和线程B各自持有一份value的副本,值为3。
- 线程A通过
getIntVolatile(var1, var2)
拿到value值3,这时线程A被挂起。 - 线程B也通过
getIntVolatile(var1, var2)
方法获取到value值3,运气好,线程B没有被挂起,并执行compareAndSwapInt
方法比较内存值也为3,成功修改内存值为2。 - 这时线程A恢复,执行
compareAndSwapInt
方法比较,发现自己手里的值(3)和内存的值(2)不一致,说明该值已经被其它线程提前修改过了,那只能重新来一遍了。 - 重新获取value值,因为变量value被volatile修饰,所以其它线程对它的修改,线程A总是能够看到,线程A继续执行
compareAndSwapInt
进行比较替换,直到成功。
整个过程中,利用CAS保证了对于value的修改的并发安全,继续深入看看Unsafe类中的compareAndSwapInt方法实现。
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
Unsafe类中的compareAndSwapInt,是一个本地方法,该方法的实现位于unsafe.cpp
中
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
- 先通过index_oop_from_field_offset_long方法取得待更新值的内存地址
- 通过Atomic::cmpxchg方法实现比较并替换,其中,x是需要更新成的新值,addr是待修改值的内存地址,e是旧值
Atomic::cmpxchg方法实现:
在Linux的x86架构下:
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
int mp = os::is_MP();
__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory");
return exchange_value;
}
#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "
其中volatile是禁止编译器优化的,即随后指令保持原样执行
在Windows的x86架构下:
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
int mp = os::isMP(); //判断是否是多处理器
_asm {
mov edx, dest
mov ecx, exchange_value
mov eax, compare_value
LOCK_IF_MP(mp)
cmpxchg dword ptr [edx], ecx
}
}
// Adding a lock prefix to an instruction on MP machine
// VC++ doesn't like the lock prefix to be on a single line
// so we can't insert a label after the lock prefix.
// By emitting a lock prefix, we can define a label after it.
#define LOCK_IF_MP(mp) __asm cmp mp, 0 \
__asm je L0 \
__asm _emit 0xF0 \
__asm L0:
LOCK_IF_MP
根据当前系统是否为多核处理器决定是否为cmpxchg指令添加lock前缀。
- 如果是多处理器,为cmpxchg指令添加lock前缀。
- 反之,就省略lock前缀。(单处理器会不需要lock前缀提供的内存屏障效果)
intel手册对lock前缀的说明如下:
- 确保后续指令执行的原子性。
在Pentium及之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其它处理器暂时无法通过总线访问内存,很显然,这个开销很大。在新的处理器中,Intel使用缓存锁定来保证指令执行的原子性,缓存锁定将大大降低lock前缀指令的执行开销。 - 禁止该指令与前面和后面的读写指令重排序。
- 把写缓冲区的所有数据刷新到内存中。
上面的第2点和第3点所具有的内存屏障效果,保证了CAS同时具有volatile读和volatile写的内存语义。
关于内存屏障及lock前缀的解释,在关于volatile关键字的文章中会有介绍。
"ABA"问题解决——AtomicStampedReference:
"ABA问题"在CAS算法中有提及,在原子类下有一个类叫AtomicStampedReference,可以很好的解决"ABA问题":
其实这个类没有什么特别厉害的功能,说白了就是在CAS算法之上加了一个标记(版本号,时间戳,随便怎么叫。。。),使得多线程修改值时不再仅仅只依靠旧的值来判断其他线程是否修改过该值。
//关键代码
public class AtomicStampedReference {
private static class Pair {
final T reference; //维护对象引用
final int stamp; //用于标志版本
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static Pair of(T reference, int stamp) {
return new Pair(reference, stamp);
}
}
private volatile Pair pair;
....
/**
* expectedReference :更新之前的原始值
* newReference : 将要更新的新值
* expectedStamp : 期待更新的标志版本
* newStamp : 将要更新的标志版本
*/
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair current = pair; //获取当前pair
return
expectedReference == current.reference && //原始值等于当前pair的值引用,说明值未变化
expectedStamp == current.stamp && // 原始标记版本等于当前pair的标记版本,说明标记未变化
((newReference == current.reference &&
newStamp == current.stamp) || // 将要更新的值和标记都没有变化
casPair(current, Pair.of(newReference, newStamp))); // cas 更新pair
}
}
总结:
形如AtomicXXX的类被称为原子类,其通过Unsafe中的本地方法实现了无锁CAS算法,而在本地方法中,通过添加lock前缀保证了在多处理器下数据的同步安全(奔腾及以前锁总线,较新处理器锁缓存),而由于其采用无锁CAS算法,又是通过本地方法实现的(本地方法一般具有相当可观的效率),所以其可以实现高效率(和synchronized对比)的原子(和volatile对比)操作。
而AtomicStampedReference通过多维护一个标记来避免了CAS算法中最常见的"ABA问题"。
参考资料:面试必问的CAS,要多了解