欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

CAS机制与自旋锁

程序员文章站 2022-11-27 16:17:34
CAS(Compare-and-Swap),即比较并替换,java并发包中许多Atomic的类的底层原理都是CAS。 它的功能是判断内存中某个地址的值是否为预期值,如果是就改变成新值,整个过程具有原子性。 具体体现于sun.misc.Unsafe类中的native方法,调用这些native方法,JV ......

cas(compare-and-swap),即比较并替换,java并发包中许多atomic的类的底层原理都是cas。

它的功能是判断内存中某个地址的值是否为预期值,如果是就改变成新值,整个过程具有原子性。

具体体现于sun.misc.unsafe类中的native方法,调用这些native方法,jvm会帮我们实现汇编指令,这些指令是cpu的原子指令,因此具有原子性。

 1 public class casdemo {
 2 
 3     public static void main(string[] args) {
 4 
 5         //初始值5
 6         atomicinteger atomicinteger = new atomicinteger(5);
 7 
 8         //和5比较,设置为10
 9         system.out.println("预期值:5,当前值:"+atomicinteger);
10         system.out.println("是否设置成功:"+atomicinteger.compareandset(5, 10));
11         //和5比较,设置为15
12         system.out.println("预期值:5,当前值:"+atomicinteger);
13         system.out.println("是否设置成功:"+atomicinteger.compareandset(5, 15));
14 
15         system.out.println("当前值:"+atomicinteger);
16     }
17 }

输出为:

预期值:5,当前值:5
是否设置成功:true
预期值:5,当前值:10
是否设置成功:false
当前值:10

下面看一下getandaddint在底层unsafe类中的代码(自旋锁),运用到了cas

//va1为对象,var2为地址值,var4是要增加的值,var5为当前地址中最新的值
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; }

首先通过volatile的可见性,取出当前地址中的值,作为期望值。如果期望值与实际值不符,就一直循环获取期望值,直到set成功。

适用场景:

  1. cas 适合简单对象的操作,比如布尔值、整型值等;

  2. cas 适合冲突较少的情况,如果太多线程在同时自旋,那么长时间循环会导致 cpu 开销很大;

cas的缺点:

  1. cpu开销过大 : 在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给cpu带来很到的压力。

  2. 不能保证代码块的原子性:cas机制所保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。

  3.  aba问题:如果内存地址v初次读取的值是a,在cas等待期间它的值曾经被改成了b,后来又被改回为a,那cas操作就会误认为它从来没有被改变过。

aba问题以及解决:使用带版本号的原子引用atomicstampedrefence<v>,或者叫时间戳的原子引用,类似于乐观锁。

 0 // aba问题及解决方式
1 public class abademo { 2 3 private static atomicreference<string> atomicreference = new atomicreference<>("a"); 4 private static atomicstampedreference<string> stampreference = new atomicstampedreference<>("a",1); 5 6 public static void main(string[] args){ 7 new thread(()->{ 8 //获取到版本号 9 int stamp = stampreference.getstamp(); 10 system.out.println("t1获取到的版本号:"+stamp); 11 try { 12 //暂停1秒,确保t1,t2版本号相同 13 timeunit.seconds.sleep(1); 14 } catch (interruptedexception e) { 15 e.printstacktrace(); 16 } 17 atomicreference.compareandset("a","b"); 18 atomicreference.compareandset("b","a"); 19 20 stampreference.compareandset("a","b",stamp,stamp+1); 21 stampreference.compareandset("b","a",stamp+1,stamp+2); 22 system.out.println("t1线程aba之后的版本号:"+stampreference.getstamp()); 23 24 },"t1").start(); 25 26 new thread(()->{ 27 //获取到版本号 28 int stamp = stampreference.getstamp(); 29 system.out.println("t2获取到的版本号:"+stamp); 30 try { 31 //暂停2秒,等待t1执行完成aba 32 timeunit.seconds.sleep(2); 33 } catch (interruptedexception e) { 34 e.printstacktrace(); 35 } 36 system.out.print("普通原子类无法解决aba问题: "); 37 system.out.println(atomicreference.compareandset("a","c")+"\t"+atomicreference.get()); 38 system.out.print("版本号的原子类解决aba问题: "); 39 system.out.println(stampreference.compareandset("a","c",stamp,stamp+1)+"\t"+stampreference.getreference()); 40 41 },"t2").start(); 42 } 43 }

输出结果:普通原子引用类在另一个线程完成aba之后继续修改(把a改成了c),带版本号原子引用有效的解决了这个问题。

t1获取到的版本号:1
t2获取到的版本号:1
t1线程aba之后的版本号:3
普通原子类无法解决aba问题: true    c
版本号的原子类解决aba问题: false    a