CAS的底层实现
CAS: compare and swap(compare and exchange),比较并交换,读取内存中的数值,计算修改后,将修改前的值与内存值比较,如果相同,则说明没有其他线程修改过,就将修改后的值写入内存;如果不同,则说明被改过,重复上述过程,直到写入成功,听着有点蒙,没关系,看一张图:
这是干嘛用的?其实它是用来替换以前的重量级锁的,说到这,又会提到一个概念,叫重量级锁,在jdk早期,synchronized本身都是用重量级锁实现,到后来jdk1.6才做了优化完善,所以你要想了解synchronized底层原理,先从重量级锁学起。
重量级锁它重在什么地方,平时我们写个锁synchornized(o)
很简单,为什么说是重量级锁?
要了解它首先你得了解操作系统,在早期的操作系统,应用层是可以直接访问系统底层硬件的(比如内存、显卡等),所以操作不当就会经常把操作系统搞蓝屏,后来操作系统为了避免这个问题,不允许你应用层直接访问我硬件,必须经过我操作系统内核进行调度。也叫做用户态和内核态。当JVM里的一段Java代码想要申请一把操作系统的锁,对于操作系统而言,你jvm就是个普通的应用程序,必须要向我操作系统申请,经过操作系统的申请做线程调度的,我们叫它重量级。
你可能会有疑问,经过操作系统内核的就叫重量级?哪体现出来的?
这里有段汇编代码
;hello.asm
;write(int fd, const void *buffer, size_t nbytes)
section data
msg db "Hello", 0xA
len equ $ - msg
section .text
global _start
_start:
mov edx, len
mov ecx, msg
mov ebx, 1 ;文件描述符1 std_out
mov eax, 4 ;write函数系统调用号 4
int 0x80
mov ebx, 0
mov eax, 1 ;exit函数系统调用号
int 0x80
这里有个著名的0x80中断,申请个锁惊动系统内核80中断,这叫做重量级。
相反,不经过操作系统内核就能在用户层自行解决的叫轻量级锁。
CAS究竟干了什么事呢?举个例子,假如有两个线程想对一个值【V】进行+1操作,第一个进程进来之后,先把这个值V【0】取出来,进行+1操作,然后再把+1后的结果【1】写回去之前,我先判断这个值是否依然为0,如果我发现这个值不是0了,变成【2】了,那么我就知道这肯定是有其他线程将这个值改掉了,这时候我不去写这个值,而是把这个值【2】再次读出来,再进行+1操作,写之前我判断这个值V是否为【2】,如果不为2,我就一直循环下去,直到成功为止。这个过程是不需要加锁的,所以说CAS叫无锁,又叫自旋锁。
看到这你可能有个疑问,如果我这个线程将值改成【1】后,在我往回写的过程之中,有一个线程将这个值【0】取走了,改成【1】,写回去,又有一个线程将值【1】取走了,改成了【0】,这个值从0 -->1 --> 0,如果是简单的值从0到1又回到0,这种没问题,不影响最终的结果,但是如果值是引用类型,不允许中间有人改过,那这就有问题,这就是CAS的ABA问题。
怎么解决?加版本号,举个例子,比如说你和你的女朋友分手了,分手之后她又经历了别的男人,又回到了你的身边,但是你发现她和以前不太一样了,那么你怎么察觉这一点?很简单,她离开的时候在她脑门上贴个标签1.0,当她再回来的时候,发现脑门上是99.0,那你就知道她到底经历了什么。
如果问你JDK具体的实现,BooleanReference, 用布尔值标注它有没有改变或版本号version标注。
看到这你可能又会问,你CAS不是比较并交换吗,如果一个线程比较完之后,将要写回的过程,另一个线程将这个值改了,那你不是傻了吗。其实最后这一步也就是写回的操作是原子操作,是不允许打断的,那么这个原子操作是怎么实现的呢?
如果我想实现一个功能,对一个值进行自增操作,创建100个线程,每个线程都对这个值进行10000次自增,那么期待值就是1000000,先用synchronized实现:
private static int m = 0;
public static void main(String[] args) throws Exception {
Thread[] threads = new Thread[100];
CountDownLatch latch = new CountDownLatch(threads.length);
Object o = new Object();
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
synchronized (o) {
for (int j = 0; j < 10000; j++) {
m++;
}
latch.countDown();
}
});
}
Arrays.stream(threads).forEach(Thread::start);
latch.await();
System.out.println(m);
}
用CAS实现:
private static AtomicInteger m = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[100];
CountDownLatch latch = new CountDownLatch(threads.length);
for (int i = 0; i < 100; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 10000; j++) {
m.incrementAndGet();
}
});
latch.countDown();
}
Arrays.stream(threads).forEach(Thread::start);
latch.await();
System.out.println(m);
}
AtomicInteger底层都是CAS实现的,CountDownLatch叫门栓,latch.await();
的意思就是把门栓打开,把门堵住,想进来必须拿到锁,必须等签名100把锁latch.countDown()
到0了才能执行下面的操作,等同于thread.join();
说跑题了,这里的核心是m.incrementAndGet();
,它没有加锁是怎么保证数据一致性的?简单,进源码看下:
/**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
你会发现它调用了unsafe类的getAndAddInt方法,点进去:
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;
}
compareAndSwapInt,这个方法怎么看着那么熟悉,What?这不就是CAS吗?点进去:
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
很不幸,只能到这里了,看到了native
,这是调用了C++代码的实现,如果你想继续往下看,那你只能看UnSafe的底层C++代码了,上网搜unsafe.cpp
compareAndSetInt代码实现:
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSetInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)) {
oop p = JNIHandles::resolve(obj);
//获取对象的变量的地址
jint* addr = (jint *)index_oop_from_field_offset_long(p, offset);
//调用Atomic操作
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
} UNSAFE_END
发现最终调用了cmpxchg
方法
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;
}
这里已经是底层汇编实现了,看到LOCK_IF_MP
,如果mp为true,就Lock,那么我们看下is_MP
方法
static inline bool is_MP() {
// During bootstrap if _processor_count is not yet initialized
// we claim to be MP as that is safest. If any platform has a
// stub generator that might be triggered in this phase and for
// which being declared MP when in fact not, is a problem - then
// the bootstrap routine for the stub generator needs to check
// the processor count directly and leave the bootstrap routine
// in place until called after initialization has ocurred.
return (_processor_count != 1) || AssumeMP;
}
看到_processor_count != 1
进程数不等于1,什么意思呢,就是不是单核的,再看下LOCK_IF_MP
方法
// Adding a lock prefix to an instruction on MP machine
#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "
从注释翻译出来, 添加一个“lock”的前缀,在哪添加呢?在某一指令上,MP的意思是multi processor,多核机器,也就是在多核上添加一个lock的前缀。所以最终汇编调用的指令是lock cmpxchg
,这条汇编的指令的作用就是锁总线。锁总线什么意思?如果多CPU也就是多核,未防止我写入的时候被其他CPU打断,我把CPU通往内存的一组线给锁定,不允许其他CPU去修改。
所以CAS的底层实现就是lock cmpxchg
,多核才会加锁,单核不需要。