13518219792

建站动态

根据您的个性需求进行定制 先人一步 抢占小程序红利时代

Java进阶:线程并发之深入理解CAS机制详解

前言

独占锁是一种悲观锁,synchronized就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁;

创新互联专注为客户提供全方位的互联网综合服务,包含不限于网站制作、网站设计、翁牛特网络推广、微信小程序开发、翁牛特网络营销、翁牛特企业策划、翁牛特品牌公关、搜索引擎seo、人物专访、企业宣传片、企业代运营等,从售前售中售后,我们都将竭诚为您服务,您的肯定,是我们最大的嘉奖;创新互联为所有大学生创业者提供翁牛特建站搭建服务,24小时服务热线:18982081108,官方网址:www.cdcxhl.com

而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁用到的机制就是CAS;

今天我们就来介绍下cas机制;

一、CAS介绍

1、什么是CAS

2、那些地方采用了 CAS 机制

3、synchronized 和 CAS 的区别

4、为什么需要CAS机制

我们经常使用volatile关键字修饰某一个变量,表明这个变量是全局共享的一个变量,同时具有了可见性和有序性。但是却没有原子性。比如说一个常见的操作a++。这个操作其实可以细分成三个步骤:

(1)从内存中读取a

(2)对a进行加1操作

(3)将a的值重新写入内存中

在单线程状态下这个操作没有一点问题,但是在多线程中就会出现各种各样的问题了。因为可能一个线程对a进行了加1操作,还没来得及写入内存,其他的线程就读取了旧值。造成了线程的不安全现象;

Volatile关键字可以保证线程间对于共享变量的可见性可有序性,可以防止CPU的指令重排序(DCL单例),但是无法保证操作的原子性,所以jdk1.5之后引入CAS利用CPU原语保证线程操作的院子性;

CAS操作由处理器提供支持,是一种原语。原语是操作系统或计算机网络用语范畴。是由若干条指令组成的,用于完成一定功能的一个过程,具有不可分割性,即原语的执行必须是连续的,在执行过程中不允许被中断。如 Intel 处理器,比较并交换通过指令的 cmpxchg 系列实现;

二、cas底层实现

1、底层依靠Unsafe的CAS操作来保证原子性;

CAS的实现主要在JUC中的atomic包,我们以AtomicInteger类为例:

 
 
 
 
  1. /** 
  2.  * Atomically adds the given value to the current value. 
  3.  * 
  4.  * @param delta the value to add 
  5.  * @return the previous value 
  6.  */ 
  7. public final int getAndAdd(int delta) { 
  8.     return unsafe.getAndAddInt(this, valueOffset, delta); 
  9. public final int incrementAndGet() { 
  10.     for (;;) { 
  11.         int current = get(); 
  12.         int next = current + 1; 
  13.         if (compareAndSet(current, next)) 
  14.             return next; 
  15.     } 
  16. private volatile int value; 
  17. public final int get() { 
  18.     return value; 

代码是一个无限循环,也就是CAS的自旋。循环体当中做了三件事:

获取当前值;

当前值+1,计算出目标值;

进行CAS操作,如果成功则跳出循环(当前值和目标值相等),如果失败则重复上述步骤;

2、Unsafe.class

 
 
 
 
  1. public final int getAndAddInt(Object var1, long var2, int var4) { 
  2.     int var5; 
  3.     do { 
  4.         var5 = this.getIntVolatile(var1, var2); 
  5.     } while (!this.compareAndSwapInt(var1, var2, var5, var5 + var4));//native方法 
  6.     return var5; 
  7. }    
  8. ******** 
  9. public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);//底层c++实现 

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);//底层c++实现

3、compareAndSwapInt为native方法,对应底层hotspot虚拟机unsage.cpp

 
 
 
 
  1. UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)) 
  2.   UnsafeWrapper("Unsafe_CompareAndSwapInt"); 
  3.   oop p = JNIHandles::resolve(obj); 
  4.   jint* addr = (jint *) index_oop_from_field_offset_long(p, offset); 
  5.   return (jint)(Atomic::cmpxchg(x, addr, e)) == e; 
  6. UNSAFE_END 
  7. *** 

这里可以看到最终使用了Atomic::cmpxchg来保证原子性,可继续跟进代码

4、Atomic::cmpxchg针对不同平台有不同的实现方式

 
 
 
 
  1. *** 
  2. // Adding a lock prefix to an instruction on MP machine 
  3. #define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: " 
  4. *** 
  5. inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) { 
  6.   int mp = os::is_MP(); 
  7.   __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)" 
  8.                     : "=a" (exchange_value) 
  9.                     : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp) 
  10.                     : "cc", "memory"); 
  11.   return exchange_value; 

最重要的指令为 LOCK_IF_MP , MP是指多CPU(multi processors),最终意义为多CPU的情况下需要lock,通过lock的方式来保证原子;

lock解释:

总之:JAVA中我们使用到涉及到CAS操作的底层实现为对应平台虚拟机中的c++代码(lock指令)实现来保证原子性;

三、CAS 的缺点及解决方式

CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题。ABA问题,循环时间长开销大和只能保证一个共享变量的原子操作;

1、ABA问题:因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A, 那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了;

ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A;

从Java1.5开始JDK的atomic包里提供了一个类 AtomicStampedReference 来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值;

2、循环时间长开销大:在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,即自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销;

如果JVM能支持处理器提供的pause指令,那么效率会有一定的提升。pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率;

代码层面,破坏掉for死循环,当自旋超过一定时间或者一定次数时,return退出;

使用类似ConcurrentHashMap的方法。当多个线程竞争时,将粒度变小,将一个变量拆分为多个变量,达到多个线程访问多个资源的效果,最后再调用sum把它合起来,能降低CPU消耗,但是治标不治本;

3、只能保证一个共享变量的原子操作:当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性;

这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij;

从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作;

四、CAS使用的时机

线程数较少、等待时间短可以采用自旋锁进行CAS尝试拿锁,较于synchronized高效;

线程数较大、等待时间长,不建议使用自旋锁,占用CPU较高;

总结

CAS可以保证多线程对数据写操作时数据的一致性;

CAS的思想:三个参数,一个当前内存值V、旧的预期值A、即将更新的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false;


分享名称:Java进阶:线程并发之深入理解CAS机制详解
文章链接:http://cdbrznjsb.com/article/coeccpp.html

其他资讯

让你的专属顾问为你服务