编程语言
首页 > 编程语言> > Java原子类中CAS详解

Java原子类中CAS详解

作者:互联网

Java原子类中CAS详解

在Java中使用volatile关键字不保证操作的原子性从而在多线程环境下会出现问题,解决方法可以使用琐机制使用synchronized和lock进行加锁但是效率极低一般不使用这种方式解决原子性问题,在Java中的java.util.concurrent.atomic 包下有各种数据类型的原子类,使用原子类型来解决原子性问题最为高效

public class Atomicity {
    private static int count = 0;
    public static void main(String[] args) {

        for (int i = 0; i < 100; i++) {
            new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    count++;
                }
            }).start();
        }

        //Java中至少有两个线程 main 线程和 gc 线程
        while(Thread.activeCount()>2){
            Thread.yield();
        }
        System.out.println(count);
    }
}

测试结果:
测试结果
很显然没有使用原子类出现了原子性问题

使用原子类

public class Atomicity {
    private  static AtomicInteger count = new AtomicInteger(0);
    public static void main(String[] args) {

        for (int i = 0; i < 100; i++) {
            new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    count.incrementAndGet();
                }
            }).start();
        }

        //Java中至少有两个线程 main 线程和 gc 线程
        while(Thread.activeCount()>2){
            Thread.yield();
        }
        System.out.println(count);
    }
}

测试结果:
测试结果
没有出现原子性问题


原子类保证原子性是因为底层使用CAS机制,根本原因就是硬件底层执行了一条cpu指令仅凭该指令可以完成"比较并交换"一系列操作

compare and swap,解决多线程并行情况下使用锁造成性能损耗的一种机制,CAS操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在CAS指令之前返回该位置的值。CAS有效地说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。 ——《百度百科》

incrementAndGet()方法源码:

 /**
     * Atomically increments by one the current value.
     *
     * @return the updated value
     */
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }

从源码可以看出,方法内又使用了unsafe对象的getAndAddInt方法问题来了这个unsafe对象又是什么?
sun.misc 下的Unsafe类 Java不能直接管理内存而Unsafe类可以帮助我们和C++那样来访问内存,而且Unsafe类中大多都是native方法

Unsafe类中getAndAddInt方法源码:

 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;
    }

这就是原子类保证原子操作的核心代码了
首先do while循环很明显是自旋锁
然后compareAdnSwapInt是一个native方法,也就是它执行了一条cpu指令 该指令可以完成比较并且进行值交换操作,完成值的更新

unsafe.getAndAddInt(this, valueOffset, 1)
public final int getAndAddInt(Object var1, long var2, int var4)
var1代表当前的原子类对象->this对象
var2表示内存地址偏移量->valueOffset
var4则表示要增加的值 ->1

var5 = this.getIntVolatile(var1, var2)
根据传入的内存地址值 和偏移量得到原值

this.compareAndSwapInt(var1, var2, var5, var5 + var4)
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
本地方法通过c++调用底层硬件,该方法也就执行了一条指令该指令完成了"比较和交换一系列操作"

Object var1,就是传入的this对象 ->this
long var2就是内存地址偏移量->valueOffset
int var4就是内存原值->var5
int var5就是修改后的值->var5+1

ABA问题

CAS中会出现ABA问题,比如要修改A为B 但是中间进行了先将A修改为C然后又将C改回了A,最后进行A修改为B的操作的时候不知道进行了AC操作,这个未知的操作可能隐藏着问题。

解决ABA问题,使用到了乐观锁思想,每次修改就加上一个版本号做一个标记
在jdk 1.5之后为了解决ABA问题引入了AtomicStampedReferenc来解决ABA问题

示例演示了AtomicStampedReferenc可以解决ABA问题

public class ABA {
    public static void main(String[] args) throws InterruptedException {
        // 有参构造 传入初始值和初始的版本号
        AtomicStampedReference<Integer> countStamped = new AtomicStampedReference<>(1, 1);
        new Thread(()->{
            int stamp = countStamped.getStamp();
            System.out.println(Thread.currentThread().getName()+"------》期望的版本号"+stamp);
            
            //获取到期望的版本号之后让正常线程休眠  让捣乱线程先去捣乱 修改版本号
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"------》实际的版本号"+countStamped.getStamp());
            //由于捣乱线程先执行 导致版本号增加 与期望的版本号不一致导致修改失败
            System.out.println(Thread.currentThread().getName()+"执行结果:"+countStamped.compareAndSet(1,6,stamp,stamp+1));
        },"正常线程").start();

        //先让 正常线程获取到期望的版本号
        TimeUnit.SECONDS.sleep(1);

        new Thread(()->{
            //捣乱线程 先将1->2 又将2->1
            System.out.println(Thread.currentThread().getName()+"执行结果:"+countStamped.compareAndSet(1,2,countStamped.getStamp(),countStamped.getStamp()+1));

            System.out.println(Thread.currentThread().getName()+"执行结果:"+countStamped.compareAndSet(2,1,countStamped.getStamp(),countStamped.getStamp()+1));
        },"捣乱线程").start();
    }
}

测试结果:
显而易见正常线程执行失败 尽管值是我们所期望的 但是由于版本号不是我们期望的也会导致修改失败 从而解决了ABA问题
测试结果

标签:var5,Java,Thread,CAS,countStamped,版本号,int,线程,类中
来源: https://blog.csdn.net/weixin_43621226/article/details/113702317