CAS指令和具体源代码
源码说话,我们看一下getAndIncrement方法:
1 //该方法功能是Interger类型加1 2 public final int getAndIncrement() { 3 //主要看这个getAndAddInt方法 4 return unsafe.getAndAddInt(this, valueOffset, 1); 5 } 6 7 //var1 是this指针 8 //var2 是地址偏移量 9 //var4 是自增的数值,是自增1还是自增N 10 public final int getAndAddInt(Object var1, long var2, int var4) { 11 int var5; 12 do { 13 //获取内存值,这是内存值已经是旧的,假设我们称作期望值E 14 var5 = this.getIntVolatile(var1, var2); 15 //compareAndSwapInt方法是重点, 16 //var5是期望值,var5 + var4是要更新的值 17 //这个操作就是调用CAS的JNI,每个线程将自己内存里的内存值M 18 //与var5期望值E作比较,如果相同将内存值M更新为var5 + var4,否则做自旋操作 19 } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); 20 21 return var5; 22 }
解释一下getAndAddInt方法的流程:
假设有一下情景:
- A、B两个线程
- jvm主内存的值1,A、B工作内存的值为1(工作内存会拷贝一份主内存的值)
- 当前期望值为1,做加1操作
- 此时var5 = 1,var4 = 1;
- A线程将var5与工作内存值M比较,比较var5是否等于1
- 如果相同则将工作内存值修改为var5 + var4 即修改为2并同步到驻村,此时this + valueOffset指针里,示例变量value的值就是2,结束循环
- 如果不相同,则是B线程修改了主内存的值,说明B线程已经先于A线程做了加1操作,A线程没有更新成功需要继续循环,注意此时var5更新为新的内存值,假设当前的内存值是2,那么此时var5 = 2,var + var4 = 3,重复上述步骤直到成功(自旋),成功之后,内存地址中的值就改变为3
简单来说,就是将工作内存的值与主内存的值来进行比较,如果相等,说明没有被其他线程修改,则进行赋新值操作,如果不相等,说明主内存的数据被其他线程更改过,则此时需要更新工作内存的副本值,并且重新获取主内存的值,再次进行比较,直到二者相等为止;
CAS优缺点
- 优点
非阻塞的轻量级的乐观锁,通过CPU指令实现,在资源竞争不激烈的情况下性能高,相比synchronized重量锁,synchronized会进行比较复杂的加锁、解锁和唤醒操作。
- 缺点
- ABA问题: 线程C、D;线程D将A修改为B后又修改为A,此时C线程以为A没有改变过,java的原子类AtomicStampedReference,通过控制变量值的版本号来保证CAS的正确性。具体解决思路就是在变量前追加上版本号,每次变量更新的时候把版本号加一,那么A - B - A就会变成1A - 2B - 3A。
- 自旋时间过长,消耗CPU资源,如果资源竞争激烈,多线程自旋长时间消耗资源