本篇主要写下面几个东西:
什么是CAS
锁有什么劣势
在多线程并发下,可以通过加锁来保证线程安全性,但多个线程同时请求锁,很多情况下避免不了要借助操作系统,线程挂起和恢复会存在很大的开销,并存在很长时间的中断。一些细粒度的操作,例如同步容器,操作往往只有很少代码量,如果存在锁并且线程激烈地竞争,调度的代价很大。
总结来说,线程持有锁,会让其他需要锁的线程阻塞,产生多种风险和开销。加锁是一种悲观方法,线程总是设想在自己持有资源的同时,肯定有其他线程想要资源,不牢牢锁住资源还不能放心呢。
在硬件的支持下,出现了非阻塞的同步机制,其中一种常用实现就是CAS。
CAS概述
现代的处理器都包含对并发的支持,其中最通用的方法就是比较并交换(compare and swap),简称CAS。
CSA操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。
如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论V值是否等于A值,都将返回V的原值。
CAS 有效地说明了:我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置。但是无论是否更新了V的值,都会返回A的旧值,上述的过程是一个原子操作。
伪代码表示:
public class SimulationCAS {
private int value;
public synchronized int get() {
return value;
}
/*
* return true if successful. False return indicates that the actual value was
* not equal to the expected value.
*/
public synchronized boolean compareAndSet(int expectedValue, int newValue) {
if (expectedValue == compareAndSwap(expectedValue, newValue)) {
return true;
}
return false;
}
public synchronized int compareAndSwap(int expectedValue, int newValue) {
int oldValue = value;
if (oldValue == expectedValue) {
value = newValue;
}
return oldValue;
}
}
上面的代码模拟了CAS的操作,其中compareAndSwap是CAS语义的体现,compareAndSet对value进行了更新操作,并返回成功与否。
当多个线程尝试使用CAS同时更新一个变量,最终只有一个
线程会成功,其他线程都会失败。但和使用锁不同,失败的线程不会被阻塞
,而是被告之本次更新操作失败了,可以再试一次。此时,线程可以根据实际情况,继续重试或者跳过操作,大大减少因为阻塞而损失的性能。所以,CAS是一种乐观的操作,它希望每次都能成功地执行更新操作。
AtomicInteger的使用
JVM是支持CAS的,体现在我们常用的Atom原子类
我们来实现一个需求,发起20个线程,每个线程对race变量进行10000次自增操作,如果代码正确的话,最后输出的结果应该是20000。
对比下面两组代码:
1.使用传统的i++
public class VolatileTest {
public static volatile int race = 0;
public static void increase() {
race++;
}
private static final int THREADS_COUNT = 20;
public static void main(String[] args) {
Thread[] threads = new Thread[THREADS_COUNT];
for (int i = 0; i < THREADS_COUNT; i++) {
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
increase();
}
}
});
threads[i].start();
}
// 等待所有累加线程都结束
while (Thread.activeCount() > 1)
Thread.yield();
System.out.println(race);
}
}
输出如下
127291 // 这个结果每次运行都不同,但总是小于预期的200000
2.使用AtomicInteger类
public class AtomicTest {
public static AtomicInteger race = new AtomicInteger(0);
public static void increase() {
race.incrementAndGet();
}
private static final int THREADS_COUNT = 20;
public static void main(String[] args) throws Exception {
Thread[] threads = new Thread[THREADS_COUNT];
for (int i = 0; i < THREADS_COUNT; i++) {
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
increase();
}
}
});
threads[i].start();
}
while (Thread.activeCount() > 1)
Thread.yield();
System.out.println(race);
}
}
运行结果如下
200000
可见使用AtomicInteger代替int后,程序输出了正确的结果,一切都要归功于它的原子性,下面来看一下AtomicInteger的原理。
AtomicInteger的原理
代码分析基于OpenJDK1.6
incrementAndGet方法
我们先来看下AtomicInteger的incrementAndGet
// AtomicInteger.java
/**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
进入compareAndSet方法
// AtomicInteger.java
/**
* Atomically sets the value to the given updated value
* if the current value {@code ==} the expected value.
*
* @param expect the expected value
* @param update the new value
* @return true if successful. False return indicates that
* the actual value was not equal to the expected value.
*/
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
可见其中直接调用了Unsafe类的compareAndSwapInt方法
// Unsafe.java
/**
* Atomically update Java variable to <tt>x</tt> if it is currently
* holding <tt>expected</tt>.
* @return <tt>true</tt> if successful
*/
public final native boolean compareAndSwapInt(Object o, long offset,
int expected,
int x);
这是一个native方法,跟踪到hotspot中的unsafe.cpp中
// hotspot/src/share/vm/prims/unsafe.cpp
// These are the correct methods, moving forward:
static JNINativeMethod methods[] = {
...
{CC"compareAndSwapInt", CC"("OBJ"J""I""I"")Z", FN_PTR(Unsafe_CompareAndSwapInt)},
...
}
来看Unsafe_CompareAndSwapInt方法
// hotspot/src/share/vm/prims/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; // cmpxchg方法
UNSAFE_END
可以看到调用到了Atomic::cmpxchg方法,这个跟操作系统有关, 跟CPU架构也有关。
Ⅰ 如果是linux下x86的架构
// hotspot/src/os_cpu/linux_x86/vm/atomic_linux_x86.inline.hpp
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)" // 主要是cmpxchgl这个函数
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory");
return exchange_value;
}
Ⅱ 如果是windows下x86的架构
// hotspot/src/os_cpu/windows_x86/vm/atomic_windows_x86.inline.hpp
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
// alternative for InterlockedCompareExchange
int mp = os::is_MP();
__asm {
mov edx, dest
mov ecx, exchange_value
mov eax, compare_value
LOCK_IF_MP(mp)
cmpxchg dword ptr [edx], ecx // 主要是cmpxchgl这个函数
}
}
也就是说CAS的原子性实际上是CPU指令集实现的,因为我们需要操作和冲突检测这两个步骤具备原子性。如果在这里再使用互斥同步来保证就失去意义了,所以我们只能靠硬件来完成这件事情。
getAndIncrement方法
跟incrementAndGet的区别就是一个是返回current,一个是返回next(int next = current + 1;)的。可理解为getAndIncrement()是i++,而incrementAndGet()是++i。
/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return current;
}
}
总结
现在已经了解乐观锁及CAS相关机制,乐观锁避免了悲观锁独占对象的现象,同时也提高了并发性能,但它也有缺点:
观锁只能保证一个共享变量的原子操作。如上例子,自旋过程中只能保证value变量的原子性,这时如果多一个或几个变量,乐观锁将变得力不从心,但互斥锁能轻易解决,不管对象数量多少及对象颗粒度大小。
长时间自旋可能导致开销大。假如CAS长时间不成功而一直自旋,会给CPU带来很大的开销。
ABA问题。CAS的核心思想是通过比对内存值与预期值是否一样而判断内存值是否被改过,但这个判断逻辑不严谨,假如内存值原来是A,后来被一条线程改为B,最后又被改成了A,则CAS认为此内存值并没有发生改变,但实际上是有被其他线程改过的,这种情况对依赖过程值的情景的运算结果影响很大。解决的思路是引入版本号,每次变量更新都把版本号加一。
乐观锁是对悲观锁的改进,虽然它也有缺点,但它确实已经成为提高并发性能的主要手段,而且jdk中的并发包也大量使用基于CAS的乐观锁。
最后附一张流程图方便代码梳理,点击看大图
参考资料
https://www.jianshu.com/p/e2179c74a2e4
https://www.cnblogs.com/coderland/p/5902956.html