原子操作类AtomicInteger详解——由valatile引发的思考CAS初体验

一、导读

在我的上一篇《并发高级之详解volatile——你看到了多少假象》留了一个问题:volatile保证不了原子性,那我们应该怎么保证原子性?
1.加锁 synchronized同步代码块或者lock锁(非本次讨论重点)
2.原子操作类AtomicInteger(本文重点)

先回顾一下volatile修饰的变量不保证原子性:

public class Data {
    public volatile int a = 1;

    public void add(){
         ++a;
    }
}
public class Atomicity {
    public static void main(String[] args) {
        Data data = new Data();
       
        //100*100=10000
        for (int i=0;i <100;i++){
            new Thread(() ->{
                for (int j=0;j <100;j++){
                    data.add();
                }
            }).start();
        }
        //防止上面的线程没有全部跑完,对结果造成干扰activeCount>2 是因为除了主线程之外还有一个守护线程
        while(Thread.activeCount() > 2){
            Thread.yield();
        }
        System.out.println(data.a);
    }
}

如果++a是原子性的,理论上打印出来的a值应该为1+100*100=10001,但是结果却不是。具体原因没看过我上一博客《并发高级之详解volatile——你看到了多少假象》的同学可以自行观看,下面介绍AtomicInteger怎么实现原子性

二、AtomicInteger

那么AtomicInteger怎么实现原子性呢?代码修改如下

public class AtomicData {
	//a 的定义采用AtomicInteger 
    public  AtomicInteger a = new AtomicInteger(1);
    public void add(){
        a.getAndIncrement();
    }
}
public class Atomicity {
    public static void main(String[] args) {
        //Data data = new Data();
        AtomicData data= new AtomicData();
        //100*100=10000
        for (int i=0;i <100;i++){
            new Thread(() ->{
                for (int j=0;j <100;j++){
                    data.add();
                }
            }).start();
        }
        //防止上面的线程没有全部跑完,对结果造成干扰activeCount>2 是因为除了主线程之外还有一个守护线程
        while(Thread.activeCount() > 2){
            Thread.yield();
        }
        System.out.println(data.a.get());
    }
}

以上代码真正做到了原子操作,无论执行多少次结果都是10001,a.getAndIncrement() 保证了原子性,那么getAndIncrement()方法又是如何保证原子性的呢?
源码:

/**
 * Atomically increments by one the current value.
 *
 * @return the previous value
 */
public final int getAndIncrement() {
    //valueOffset  该变量值在内存中的偏移地址
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

private static final long valueOffset;
//初始化偏移地址
static {
    try {
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}

可以看到低层调用了unsafe类的getAndAddInt()方法。
unsafe类是什么呢?

unsafe类存在于sun.misc包,是CAS的核心类,java可以通过这个类直接操作低层。
Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力,一旦能够直接操作内存,这也就意味着:

(1)不受jvm管理,也就意味着无法被GC,需要我们手动GC,稍有不慎就会出现内存泄漏。

(2)Unsafe的不少方法中必须提供原始地址(内存地址)和被替换对象的地址,偏移量要自己计算,一旦出现问题就是JVM崩溃级别的异常,会导致整个JVM实例崩溃,表现为应用程序直接crash掉。
(3)直接操作内存,也意味着其速度更快,在高并发的条件之下能够很好地提高效率。

继续往下看: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;
}

先根据当前对象和偏移地址获得当前对象在内存中的最新值var5,然后调用compareAndSwapInt()方法。而compareAndSwapInt就是大名鼎鼎的CAS(Compare and Swap)

三、CAS(Compare and Swap)

1、什么是CAS?

CAS:Compare and Swap,即比较再交换。

jdk5增加了并发包java.util.concurrent.*,
其下面的类使用CAS算法实现了区别于synchronouse同步锁的一种乐观锁。
JDK 5之前Java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是是悲观锁。

2、CAS算法理解

对CAS的理解,CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

下面详解

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
    //根据当前对象和偏移地址获得当前对象在内存中的最新值
        var5 = this.getIntVolatile(var1, var2);
        //执行compareAndSwapInt方法时,内存中的最新值可能已经被别的线程改变,此时会返回false,则继续循环,若内存中的最新值和var5获得的期望值一致,则修改成功并返回true,退出循环
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

compareAndSwapInt(var1, var2, var5, var5 + var4)有四个参数:
var1:当前对象
var2:当前对象在内存中的偏移地址
var5:当前对象在内存中的最新值(旧的期望值)
var5 + var4:旧的期望值和内存值相同时,把旧的期望值修改为新值var5 + var4

最开始有个疑惑?compareAndSwapInt为什么就是原子操作呢?如果返回结果之前,内存值被别的线程修改了呢?
因为cas方法是本地的,是基于汇编的系统原语,原语的执行是连续的不允许中断,所以会紧接着就返回,不会再被中断进程被修改数据,保证数据一致性,且保证并发性,比synchonized并发性要好

场景举例:
假设线程A和线程B两个线程同时执行getAndAddInt操作( 分别跑在不同CPU上) :

1.AtomicInteger里 面的value原始值为3,即主内存中AtomicInteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的副本在各自的本地内存中

2.线程A通过getIntVolatile(var1, var2)拿到value值3,这时线程A被挂起。

3.线程B也通过getlntVolatile(var1, var2)方法获取到value值3,此时刚好线程B没有被挂起并执行compareAndSwaplnt方法因为内存值和期望值都是3,成功修改内存值为4.

4.这时线程A恢复,执行compareAndSwaplnt方法比较, 发现自己手里的值数字3和主内存的值数字4不- -致, 说明该值已经被别的线程修改过了,修改失败,只能继续在循环中不断尝试。

5.线程A重 新获取value值, 因为变量value被volatile修饰,所以其它线程对它的修改, 线程A总是能够看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功。

CAS的缺点:
1.当修改不成功时则一直处于循环中,造成较大开销
2.只能保证一个共享变量的原子性
3.从读取期望值到进行CAS操作之间是有时间差的会造成ABA问题

ABA问题:线程一读到共享变量值为A,再进行CAS之前这段时间里,其它线程把A更新为B又更新为A,然后线程1进行CAS操作,发现内存值与期望值都是A,跟新成功,并不知道A被修改过

那么怎么解决ABA问题呢?

原子引用 Atomicreference
Atomicreference可以接收一个对象将其包装为原子类,这样就可以对这个对象进行原子操作

AtomicStampedReference带时间戳的原子引用
构造函数需要一个初始的应用对象,一个时间戳(任意int类型即可,不一定是时间戳)

/**
 * Creates a new {@code AtomicStampedReference} with the given
 * initial values.
 *
 * @param initialRef the initial reference
 * @param initialStamp the initial stamp
 */
public AtomicStampedReference(V initialRef, int initialStamp) {
    pair = Pair.of(initialRef, initialStamp);
}

我们可以在进行CAS的时间考虑上时间戳(可以理解为版本号,与乐观锁的版本号思想一致),改进代码如下

public class AtomicStampedRdferenceDemo {

    public static void main(String[] args) {
        AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference(100,1);
        new Thread(() -> {
            try {
                //保证T2线程开启
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            int stamp = atomicStampedReference.getStamp();
            System.out.println(atomicStampedReference.getReference());
            atomicStampedReference.compareAndSet(100,101,stamp,stamp+1);
            System.out.println(atomicStampedReference.getReference());
            atomicStampedReference.compareAndSet(101,100,stamp,stamp+1);
        },"T1").start();
        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            int ref = atomicStampedReference.getReference();
            try {
                //保证T1线程完成一次ABA操作
                Thread.sleep(4000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(atomicStampedReference.compareAndSet(100,99,stamp,stamp+1));
        },"T2").start();

    }
}

T1线程暂停一秒保证T2线拿到期望值,T2暂停4秒保证T1后续对原对象进行了一次ABA操作,这样T2进行CAS时虽然内存值与期望值一致,但是时间戳不一致,也不会成功
实验结果:
在这里插入图片描述
思考:AtomicInteger保证了原子性,是如何保证可见性的?

public AtomicInteger(int initialValue) {
    value = initialValue;
}
private volatile int value;

可以看到value低层还是由volatile来修饰的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值