JAVA语言模拟CAS的ABA问题

    技术2023-10-08  98

    什么是CAS机制

    CAS是英文单词Compare And Swap的缩写,翻译过来就是比较和替换。

    CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

    更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

     

    如何保证多核心下的线程安全?

    系统底层进行CAS操作的时候,会判断当前系统是否是多核心系统,如果是就给"总线"加锁,只有一个线程能对总线加锁成功,加锁成功之后会进行CAS操作,CAS的原子性是平台级别的.

    CAS的缺点

    ABA问题

    CAS需要在操作值的时候检查下值有没有变化,如果没有变化则更新,但是如果一个值原来是A,在CAS方法执行之前,被其它线程修改为了B,然后又修改回了A,那么CAS方法检查的时候就会发现它的值没有发生变化,但是实际上它发生了变化,这就是CAS的ABA问题.

    下面用一个实例代码模拟ABA问题:

    package com.springcloud.server.springserver.thread; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; public class ABADemo { public static AtomicInteger a = new AtomicInteger(1); public static void main(String[] args) { Thread main = new Thread(new Runnable() { @Override public void run() { System.out.println("操作线程"+Thread.currentThread().getName()+", 初始值"+a.get()); try { int expectNum = a.get(); int newNum = expectNum + 1; TimeUnit.SECONDS.sleep(1);//休眠一秒钟,让出cpu boolean b = a.compareAndSet(expectNum, newNum); System.out.println("操作线程"+Thread.currentThread().getName()+",CAS操作: "+b); } catch (InterruptedException e) { e.printStackTrace(); } } }, "主线程"); Thread other = new Thread(new Runnable() { @Override public void run() { try { TimeUnit.MICROSECONDS.sleep(20); a.incrementAndGet(); System.out.println("操作线程" + Thread.currentThread().getName() + "increment,值: " + a.get()); a.decrementAndGet(); System.out.println("操作线程" + Thread.currentThread().getName() + "decrement,值: " + a.get()); } catch (InterruptedException e) { e.printStackTrace(); } } }, "other"); main.start(); other.start(); } }

    输出结果:

    在第一个线程获取值然后进行CAS操作的过程中,other线程将值该为2,然后又改回1,此时main线程进行CAS判断的时候并没有发现a的值有变化,这就是典型的ABA问题.

    解决ABA问题

    使用版本号机制,给值加一个修改的版本号,每次值变化,都会修改它的版本号,CAS操作时都会去对比此版本号.

    JAVA8中ABA的解决办法:使用AtomicStampedReference,先上代码,再解释:

    package com.springcloud.server.springserver.thread; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicStampedReference; public class ABAStampDemo { public static AtomicStampedReference<Integer> a = new AtomicStampedReference<Integer>(1,1); public static void main(String[] args) { Thread main = new Thread(new Runnable() { @Override public void run() { System.out.println("操作线程"+Thread.currentThread().getName()+", 初始值"+a.getReference()); try { Integer expectNum = a.getReference(); Integer newNum = expectNum + 1; Integer stampOld = a.getStamp(); Integer stampNew = stampOld+1; TimeUnit.SECONDS.sleep(1);//休眠一秒钟,让出cpu boolean b = a.compareAndSet(expectNum, newNum,stampOld,stampNew); System.out.println("操作线程"+Thread.currentThread().getName()+",CAS操作: "+b +",stamp:"+ a.getStamp()); } catch (InterruptedException e) { e.printStackTrace(); } } }, "主线程"); Thread other = new Thread(new Runnable() { @Override public void run() { try { TimeUnit.MICROSECONDS.sleep(20); a.compareAndSet(a.getReference(),a.getReference()+1,a.getStamp(),a.getStamp()+1); System.out.println("操作线程" + Thread.currentThread().getName() + "increment,值: " + a.getReference()+",stamp:"+ a.getStamp()); a.compareAndSet(a.getReference(),a.getReference()-1,a.getStamp(),a.getStamp()+1); System.out.println("操作线程" + Thread.currentThread().getName() + "decrement,值: " + a.getReference()+",stamp:"+ a.getStamp()); } catch (InterruptedException e) { e.printStackTrace(); } } }, "other"); main.start(); other.start(); } }

    输出结果:

    AtomicStampedReference的构造方法里面,第一个是初始值,第二个是初始化的版本号,使用compareAndSet方法的时候,需要传入四个参数:

    expectedReference:期望值 newReference:新的值 expectedStamp:期望的版本号 newStamp:新的版本号

    只有当期望值和从内存里面读取的值一样,而且期望的版本号和当前内存里面的版本号一样的时候,新的值和版本号才会更改,方法返回true,否则返回false,在并发程度高而且不能忽视ABA问题的场景下,可以使用这个类进行问题的解决.

    更多关于版本号的理解,参考另外一篇博文:

    https://blog.csdn.net/thetimelyrain/article/details/100974565

    Processed: 0.029, SQL: 10