JUC学习笔记15-彻底搞懂CAS&原子引用

CAS

CAS的介绍

CAS( CompareAndSwarp ,比较并交换)是一个CPU的并发原语,原语是属于操作系统用于范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条原子指令,是个原子性操作,它的功能就是判断内存中的某个位置的值是否是预期值,如果是则更新为自己执行的新值。这个操作是原子性的,也就是说并发的时候不会出现指令重排导致数据不一致问题的现象;这个操作是基于内存级别操作的,通过unsafe类之间操作内存中的值。

package com.interview.concurrent.cas;
        import java.util.concurrent.atomic.AtomicInteger;
/**
 * @author yangxj
 * @description 描述: compareAndSet  简称 CAS 比较并交换!
 * compareAndSet(int expect, int update)
 * expect:我期望原来的值是什么,如果是,就更新为update
 * @date 2020/2/25 14:46
 */
public class AtomicIntegerDemo {

    public static void main(String[] args) {
        //参数,初始值设为5
        AtomicInteger atomicInteger = new AtomicInteger(5);

        System.out.println(atomicInteger.get()); //5
        // compareAndSet  简称 CAS 比较并交换!
        // compareAndSet(int expect, int update)  expect:我期望原来的值是什么,如果是,就更新为update
        System.out.println(atomicInteger.compareAndSet(5, 1000)); //true
        System.out.println(atomicInteger.compareAndSet(5, 1000));//false

        for (int i = 0; i < 10; i++) {
            atomicInteger.getAndIncrement();
            System.out.println(atomicInteger.get());
        }
    }
}

CAS底层原理

1、CAS底层核心:Unsafe类
atomicInteger.getAndIncrement();
在这里插入图片描述
Unsafe是CAS核心类,由于java方法无法访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为java中CAS操作的执行依赖于Unsafe类的方法。

注意:Unsafe类中所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务。

2、变量valueOffset表示在内存中偏移地址
3、变量value是用volatile修饰,保证了线程之间的内存可见性
在这里插入图片描述
var1 AtomicInteger对象本身
var2 该对象值的引用地址
var5 通过var1 var2找出主内存中的真实值
用该对象当前值与var5比较
如果相同,更新var5+var4并且返回true
如果不同,继续取值比较,直到更新完成为止
4、CAS底层是汇编
Unsafe类中的compareAndSwapInt,是一个本地方法,该方法的实现位于unsafe.cpp中

CAS的缺点

1) CPU开销过大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。
2) 不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。
3) ABA问题
什么是ABA问题:狸猫换太子
这是CAS机制最大的问题所在。(后面有介绍)

CAS引起的ABA问题(狸猫换太子)

ABA问题:CAS算法的前提是:取出内存中某个时刻的数据,比较并交换! 在这个时间差内有可能数据被修改!
比如下面的两个语句,虽然变量的值最终不发生改变,但是已经有用户操作该变量了。

        System.out.println(atomicInteger.compareAndSet(20, 21));
        //boolean compareAndSet(int expect, int update)
        System.out.println(atomicInteger.compareAndSet(21, 20));

那么应该如何解决这个问题呢?
解决这个问题,我们需要用到乐观锁(添加版本号)来解决,我们还可以通过带时间戳的原子引AutomicReference用来解决!

package cas;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;

/**
 * Description:
 *
 * @author jiaoqianjin
 * Date: 2020/8/12 22:07
 **/

public class CasAbaAtomicStampedReference {
    /**AtomicStampedReference 注意,如果泛型是一个包装类,注意对象的引用问题
     * 正常在业务操作,这里面比较的都是一个个对象
     */
    static AtomicStampedReference<Integer> atomicStampedReference = new
            AtomicStampedReference<>(1, 1);

    // CAS compareAndSet : 比较并交换!
    public static void main(String[] args) {
        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp(); // 获得版本号
            System.out.println("a1=>" + stamp);
            // 修改操作时,版本号更新 + 1
            atomicStampedReference.compareAndSet(1, 2,
                    atomicStampedReference.getStamp(),
                    atomicStampedReference.getStamp() + 1);

            System.out.println("a2=>" + atomicStampedReference.getStamp());
            // 重新把值改回去, 版本号更新 + 1
            System.out.println(atomicStampedReference.compareAndSet(2, 1,
                    atomicStampedReference.getStamp(),
                    atomicStampedReference.getStamp() + 1));
            System.out.println("a3=>" + atomicStampedReference.getStamp());
        }, "a").start();

        // 乐观锁的原理相同!
        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp(); // 获得版本号
            System.out.println("b1=>" + stamp);
            //B线程延长一时间,让其他线程发生狸猫换太子事件
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(atomicStampedReference.compareAndSet(1, 3,
                    stamp, stamp + 1));
            System.out.println("b2=>" + atomicStampedReference.getStamp());
        }, "b").start();
    }
}

原子类介绍

原子更新基本类型类

  • AtomicBoolean: 原子更新布尔类型。
  • AtomicInteger: 原子更新整型。
  • AtomicLong: 原子更新长整型。

以上3个类提供的方法几乎一模一样,以AtomicInteger为例进行详解,AtomicIngeter的常用方法如下:

  • int addAndGet(int delta): 以原子的方式将输入的数值与实例中的值相加,并返回结果。
  • boolean compareAndSet(int expect, int update): 如果输入的值等于预期值,则以原子方式将该值设置为输入的值。
  • int getAndIncrement(): 以原子的方式将当前值加 1,注意,这里返回的是自增前的值,也就是旧值。
  • void lazySet(int newValue): 最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
  • int getAndSet(int newValue): 以原子的方式设置为newValue,并返回旧值。

原子更新引用类型

原子更新基本类型的AtomicInteger,只能更新一个值,如果更新多个值,比如更新一个对象里的值,那么就要用原子更新引用类型提供的类,Atomic包提供了以下三个类:

  • AtomicReference: 原子更新引用类型。
  • AtomicReferenceFieldUpdater: 原子更新引用类型的字段。
  • AtomicMarkableReferce: 原子更新带有标记位的引用类型,可以使用构造方法更新一个布尔类型的标记位和引用类型。
  public static AtomicReference<User> ai = new AtomicReference<User>();
  
    public static void main(String[] args) {
        User u1 = new User("pangHu", 18);
        ai.set(u1);
        User u2 = new User("piKaQiu", 15);
        ai.compareAndSet(u1, u2);
        System.out.println(ai.get().getAge() + ai.get().getName());
    }
static class User {
        private String name;
        private int age;
 
        public User(String name, int age) {
            this.name = name;
            this.age = age;
        }
 
        public String getName() {
            return name;
        }
 
        public void setName(String name) {
            this.name = name;
        }
 
        public int getAge() {
            return age;
        }
 
        public void setAge(int age) {
            this.age = age;
        }
    }
 

输出结果
piKaQiu 15

代码分析
我们把对象set放到 AtomicReference 中,然后调用 compareAndSet () 原子操作替换,原理和 AtomicInteger 相同,只是调用的是 compareAndSwapObject() 方法。

原子更新字段类

如果需要原子的更新类里某个字段时,需要用到原子更新字段类,Atomic包提供了3个类进行原子字段更新:

  • AtomicIntegerFieldUpdater: 原子更新整型的字段的更新器。
  • AtomicLongFieldUpdater: 原子更新长整型字段的更新器。
  • AtomicStampedFieldUpdater: 原子更新带有版本号的引用类型。
 //创建原子更新器,并设置需要更新的对象类和对象的属性
    private static AtomicIntegerFieldUpdater<User> ai = AtomicIntegerFieldUpdater.newUpdater(User.class, "age");
 
    public static void main(String[] args) {
 
        User u1 = new User("pangHu", 18);
        //原子更新年龄,+1
        System.out.println(ai.getAndIncrement(u1));
        System.out.println(u1.getAge());
    }
 
 
 
static class User {
        private String name;
        public volatile int age;
 
        public User(String name, int age) {
            this.name = name;
            this.age = age;
        }
 
        public String getName() {
            return name;
        }
 
        public void setName(String name) {
            this.name = name;
        }
 
        public int getAge() {
            return age;
        }
 
        public void setAge(int age) {
            this.age = age;
        }
    }

代码分析
要想原子地更新字段类需要两步。第一步,因为原子更新字段类都是抽象类,每次使用的时候必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。第二步,更新类的字段必须使用 public volatile 修饰。

输出结果
18
19

原子更新数组

  • AtomicIntegerArray: 原子更新整型数组里的元素。
  • AtomicLongArray: 原子更新长整型数组里的元素。
  • AtomicReferenceArray: 原子更新引用类型数组里的元素。

这三个类的最常用的方法是如下两个方法:

  • get(int index):获取索引为index的元素值。
  • compareAndSet(int i, int expect, int update): 如果当前值等于预期值,则以原子方式将数组位置 i 的元素设置为update值。

下面以 AtomicReferenceArray 举例如下

	static int[] value =new int[]{1,2};
	static AtomicIntegerArray ai =new AtomicIntegerArray(value);
	public static void main(String[] args) {
 
		ai.getAndSet(0,2);
		System.out.println(ai.get(0));
		System.out.println(value[0]);
    }

输出结果
3
1

JDK8新增原子类

  • DoubleAccumulator
  • LongAccumulator
  • DoubleAdder
  • LongAdder
    下面以 LongAdder 为例介绍一下,并列出使用注意事项

这些类对应把 AtomicLong 等类的改进。比如 LongAccumulator 与 LongAdder 在高并发环境下比 AtomicLong 更高效。

Atomic、Adder在低并发环境下,两者性能很相似。但在高并发环境下,Adder 有着明显更高的吞吐量,但是有着更高的空间复杂度。

LongAdder其实是LongAccumulator的一个特例,调用LongAdder相当使用下面的方式调用LongAccumulator。

sum() 方法在没有并发的情况下调用,如果在并发情况下使用会存在计数不准,下面有代码为例。

LongAdder不可以代替AtomicLong ,虽然 LongAdder 的 add() 方法可以原子性操作,但是并没有使用 Unsafe 的CAS算法,只是使用了CAS的思想。

LongAdder其实是LongAccumulator的一个特例,调用LongAdder相当使用下面的方式调用LongAccumulator,LongAccumulator提供了比LongAdder更强大的功能,构造函数其中accumulatorFunction一个双目运算器接口,根据输入的两个参数返回一个计算值,identity则是LongAccumulator累加器的初始值。

private static ExecutorService executorService = Executors.newFixedThreadPool(5);
    public static void main(String[] args) {
        for (int i = 1; i <= 100; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    counter.add(2);
                }
            });
        }
        System.out.println(counter.sum());
        System.out.println(counter);
    }

在这里插入图片描述
在这里插入图片描述
图片解释

如图LongAdder则是内部维护多个变量,每个变量初始化都0,在同等并发量的情况下,争夺单个变量的线程量会减少这是变相的减少了争夺共享资源的并发量,另外多个线程在争夺同一个原子变量时候如果失败并不是自旋CAS重试,而是尝试获取其他原子变量的锁,最后获取当前值时候是把所有变量的值累加后返回的。

//构造函数
LongAdder()
    //创建初始和为零的新加法器。
 
//方法摘要
void    add(long x)    //添加给定的值。
void    decrement()    //相当于add(-1)。
double  doubleValue() //在扩展原始转换之后返回sum()as double。
float   floatValue()  //在扩展原始转换之后返回sum()as float。
void    increment()  //相当于add(1)。
int intValue()      //返回sum()作为int一个基本收缩转换之后。
long    longValue() //相当于sum()。
void    reset()    //重置将总和保持为零的变量。
long    sum()     //返回当前的总和。
long    sumThenReset()  //等同于sum()后面的效果reset()。
String  toString()   //返回。的字符串表示形式sum()。

参考链接:Java16个原子类介绍添加链接描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值