当前位置: 首页 > 后端技术 > Java

大话CAS

时间:2023-04-01 23:28:08 Java

1。无锁的概念在谈到无锁的概念时,总是会联想到乐观主义者和悲观主义者。对于乐观主义者来说,他们认为事情总会往好的方向发展,也总认为事情总会往坏的方向发展。事情发生的概率是极小的,他们可以毫无顾忌地做事,但是对于悲观主义者来说,他们总是认为如果不及时控制事态的发展,未来将是不可逆转的,即使不可逆转的局面几乎不可能发生。这两个派系对并发编程的映射就像加锁和无锁的策略,即加锁是悲观的策略,无锁是乐观的策略,因为对于加锁的并发程序,他们总是认为总是冲突每次访问共享资源时都会发生,因此必须为每个数据操作实施锁定策略。无锁策略始终假设共享资源的访问没有冲突,线程可以不加锁不等待继续执行。一旦发现冲突,无锁策略使用一种叫做CAS的技术来保证线程执行的安全。这个CAS技术是实现无锁策略的关键。让我们进一步了解CAS技术的神奇之处。2、什么是CASCAS(CompareandSwap),即比较和替换,是多线程同步的原子指令。执行函数:CAS(V,E,N)包含3个参数V表示要更新的变量E表示期望值N表示新值假设有两个操作A和B,如果从执行A的线程来??看,当另一个线程执行B时,它要么执行了B的全部,要么根本不执行B,那么A和B对彼此是原子的。实现原子操作,可以使用锁和锁机制来满足基本需求。然而,有时候我们的需求并不是那么简单。我们需要一个更加有效和灵活的机制。synchronized关键字是一种基于阻塞的锁机制,也就是说,当一个线程拥有锁时,其他访问同一资源的线程需要等待,直到该线程释放锁。这里有一些问题:第一,如果被阻塞的线程优先级很高,很重要怎么办?其次,如果获得锁的线程永远不会释放锁怎么办?(这种情况很糟糕)。另外一种情况,如果有大量的线程在争夺资源,那么CPU就会花费大量的时间和资源来处理这些竞争。同时,可能会出现死锁等一些情况。最后,锁机制其实是一种比较粗糙的机制,粒度比较大,对于计数器这样的需求来说有点太繁琐了。实现原子操作,也可以使用目前基本支持CAS指令的处理器,但是各个厂商实现的算法不一样。每个CAS运算过程包括三个运算符:内存地址V、期望值A和新值B。运算时,如果该地址存储的值等于期望值A,则将该地址处的值赋值给新值B,否则不做任何操作。CAS的基本思想是,如果这个地址上的值等于期望值,就给它赋一个新值,否则什么都不做,而是返回原来的值。循环CAS就是循环不断的进行cas操作,直到成功。CAS是如何实现线程安全的?在语言层面没有处理,我们将其留给硬件——CPU和内存。利用CPU的多处理能力实现硬件级的阻塞,再加上volatile变量的特性,我们可以实现基于原子操作的线程安全。3.CPU指令对CAS的支持或许我们会有这样的疑惑,假设有多个线程进行CAS操作,CAS中有很多步骤,是否可以在判断V和E相同并且是即将赋值?,改变了价值。数据不一致的原因是什么?答案是否定的,因为CAS是一个系统原语,属于操作系统术语的范畴。它由若干条指令组成,用于完成某个功能的一个过程,原语的执行必须是连续的。执行过程中不允许被打断,也就是说CAS是CPU的原子指令,不会造成所谓的数据不一致问题。4.悲观锁、乐观锁说到CAS,不得不提两个专业名词:悲观锁和乐观锁。我们先来看看什么是悲观锁,什么是乐观锁。4.1悲观锁,顾名思义,就是悲观锁。它总是假设最坏的情况。每次去拿数据的时候,都以为别人会修改,所以每次拿数据的时候,都会加锁,让别人想拿数据。它会一直阻塞直到获得锁(共享资源一次只被一个线程使用,其他线程阻塞,使用完再转移给其他线程)。传统关系型数据库中使用了很多这样的锁机制,比如行锁、表锁等,读锁、写锁等,都是在操作执行前加锁。Java中的synchronized、ReentrantLock等独占锁都是悲观锁思想的实现。4.2乐观锁相反,总是假设最好的情况,每次去拿数据的时候,都认为别人不会修改,所以不会加锁,但是更新的时候,会判断别人有没有更新在此期间数据可以使用版本号机制和CAS算法来实现。乐观锁适用于多读应用类型,可以提高吞吐量。和数据库提供的write_condition机制一样,其实是提供了一种乐观锁。我们今天说的CAS就是乐观锁。因为CAS操作是乐观的,它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作一个变量时,只有一个线程获胜并更新成功,其余失败,但失败的线程不会被删除。Suspend,只是被告知失败,并允许重试,当然也允许失败的线程放弃操作。基于这个原理,即使CAS操作没有锁,它也知道其他线程对共享资源操作的影响,并进行相应的处理措施。同时从这一点也可以看出,由于无锁操作中没有锁,死锁是不可能发生的,也就是说无锁操作天生就对死锁免疫。5、CAS的优点非阻塞轻量级乐观锁,由CPU指令实现,在资源竞争不激烈的情况下具有高性能,相比于synchronize重量级悲观锁,synchronize具有复杂的加锁、解锁和唤醒线程操作..6.三大问题6.1ABA问题假设这样一个场景,当第一个线程执行CAS(V,E,U)操作时,在获取当前变量V准备修改为新值U之前,其他两个线程已经连续两次修改变量V的值,使值返回到旧值。在这种情况下,我们无法正确判断变量是否被修改,如下图所示,因为CAS在操作值时需要检查值是否存在。改变,没有就更新,但是如果一个值原来是A,变成B,再变成A,那么用CAS检查会发现它的值没有改变,但实际上已经改变了。如上图所述,线程A原值为10,线程B修改为20,但是线程C修改为10,此时线程A读取,判断为旧值,发现还是10,更新操作没有修改,但是我们知道这个值变了。这是一个典型的CASABA问题。一般来说,这种情况发生的概率比较小,不会造成什么问题。比如我们对某个数做加减法,不关心数的过程,那么就会出现ABA。问题无关紧要。但是在某些情况下,还是需要预防的,那么如何解决呢?解决Java中的ABA问题,我们可以使用下面两个原子类。6.1.1AtomicStampedReferenceAtomicStampedReference是一个带有时间戳的对象引用。每次修改后,AtomicStampedReference不仅会设置一个新值,还会记录修改的时间。AtomicStampedReference设置对象值时,对象值和时间戳都必须满足预期值才能写入成功,这也解决了重复读写时无法预知值是否被修改的困境。测试demo如下publicclassABADemo{staticAtomicIntegeratIn=newAtomicInteger(100);//初始化需要传入一个初始值和初始时间staticAtomicStampedReferenceatomicStampedR=newAtomicStampedReference(200,0);staticThreadt1=newThread(newRunnable(){@Overridepublicvoidrun(){//更新到200atIn.compareAndSet(100,200);//更新到100atIn.compareAndSet(200,100);}});staticThreadt2=newThread(newRunnable(){@Overridepublicvoidrun(){try{TimeUnit.SECONDS.sleep(1);}catch(InterruptedExceptione){e.printStackTrace();}布尔标志=atIn。compareAndSet(100,500);System.out.println("flag:"+flag+",newValue:"+atIn);}});静态Threadt3=newThread(newRunnable(){@Overridepublicvoidrun(){inttime=atomicStampedR.getStamp();//更新为200atomicStampedR.compareAndSet(100,200,time,time+1);//更新为100inttime2=atomicStampedR.getStamp();atomicStampedR.compareAndSet(200,100,time2,time2+1);}});staticThreadt4=newThread(newRunnable(){@Overridepublicvoidrun(){inttime=atomicStampedR.getStamp();System.out.println("sleept4time:"+time);try{TimeUnit.SECONDS.sleep(1);}catch(InterruptedExceptione){e.printStackTrace();}booleanflag=atomicStampedR.compareAndSet(100,500,time,time+1);System.out.println("flag:"+flag+",newValue:"+atomicStampedR.getReference());}});公共静态无效主要(字符串[]args)抛出InterruptedException{t1.start();t2.开始();t1.join();t2.join();t3.开始();t4.开始();/***输出结果:flag:true,newValue:500sleepbeforet4time:0flag:false,newValue:200*/}}对比输出结果,可以看出AtomicStampedReference类确实解决了ABA问题。简单看一下它的内部实现原理publicclassAtomicStampedReference{//通过Pair存储数据和时间戳最终印章;privatePair(Treference,intstamp){this.reference=reference;this.stamp=邮票;}staticPairof(Treference,intstamp){returnnewPair(reference,stamp);}}//内部类privatevolatilePair对,用于存储值和时间;//构造函数,创建时需要传入初始值和时间初始值publicAtomicStampedReference(VinitialRef,intinitialStamp){pair=Pair.of(initialRef,initialStamp);}}然后看它的compareAndSet方法的实现:publicbooleancompareAndSet(VexpectedReference,VnewReference,intexpectedStamp,intnewStamp){返回expectedReference==current.reference&&expectedStamp==current.stamp&&((newReference==current.reference&&newStamp==current.stamp)||cas??Pair(current,Pair.of(newReference,newStamp)));}同时比较当前数据和当前时间,只有两者相等才会执行casPair()方法。只从方法名就可以知道是一个CAS方法,最终调用的是Unsafe类中的compareAndSwapObject方法。privatebooleancasPair(Paircmp,Pairval){returnUNSAFE.compareAndSwapObject(this,pairOffset,cmp,val);}至此,我们就很清楚AtomicStampedReference的内部实现思路了。通过键值对存储数据和时间戳,更新时比较数据和时间戳。只有两者都满足预期才会调用Unsafe的compareAndSwapObject方法进行值和时间戳的替换,避免了ABA问题6.1.2AtomicMarkableReferenceAtomicMarkableReference与AtomicStampedReference的区别在于AtomicMarkableReference维护了一个boolean值标识,也就是说对于true和false两种状态的切换,经测试,该方法不能完全杜绝ABA问题的发生,但是只能降低ABA问题发生的概率。公共类ABADemo{staticAtomicMarkableReferenceatMarkRef=newAtomicMarkableReference(100,false);staticThreadt5=newThread(newRunnable(){@Overridepublicvoidrun(){booleanmark=atMarkRef.isMarked();System.out.println("mark:"+mark);//更新为200System.out.println("t5结果:"+atMarkRef.compareAndSet(atMarkRef.getReference(),200,mark,!mark));}});staticThreadt6=newThread(newRunnable(){@Overridepublicvoidrun(){booleanmark2=atMarkRef.isMarked();System.out.println("mark2:"+mark2);System.out.println("t6结果:“+atMarkRef.compareAndSet(atMarkRef.getReference(),100,mark2,!mark2));}});staticThreadt7=newThread(newRunnable(){@Overridepublicvoidrun(){booleanmark=atMarkRef.isMarked();System.out.println("睡眠前t7标记:"+mark);尝试{TimeUnit.SECONDS.sleep(1);}catch(InterruptedExceptione){e.printStackTrace();}布尔标志=atMarkRef.compareAndSet(100,500,mark,!mark);System.out.println("flag:"+flag+",newValue:"+atMarkRef.getReference());}});publicstaticvoidmain(String[]args)throwsInterruptedException{t5.start();t5.join();t6.start();t6.join();t7.开始();/***Outputresult:mark:falset5result:truemark2:truet6result:truesleepbeforet5mark:falseflag:true,newValue:500---->Success.....说明ABA问题依旧occurs*/}}AtomicMarkableReference的实现原理和AtomicStampedReference类似至此,我们也明白了,如果要彻底杜绝ABA问题的发生,应该使用AtomicStampedReference原子类来更新对象。对于AtomicMarkableReference,只能降低ABA问题出现的概率,不能消除。6.2循环时间长,开销大如果自旋CAS长时间失效,会给CPU带来非常大的执行开销。6.3只能保证一个共享变量的原子操作。当对一个共享变量进行操作时,我们可以使用循环CAS的方式来保证原子操作,但是当对多个共享变量进行操作时,循环CAS就不能保证操作的原子性了。这时候就可以使用锁了。另一个技巧是将多个共享变量组合成一个共享变量进行操作。比如有两个共享变量i=2,j=a,合并ij=2a,然后用CAS操作ij。从Java1.5开始,JDK提供了AtomicReference类来保证被引用对象之间的原子性,可以将多个变量放在一个对象中进行CAS操作。如果本文对您有帮助,欢迎关注点赞`,您的支持是我坚持创作的动力。转载请注明出处!