> 文章列表 > JUC并发编程高级篇第三章之CAS[Unsafe和原子增强类]

JUC并发编程高级篇第三章之CAS[Unsafe和原子增强类]

JUC并发编程高级篇第三章之CAS[Unsafe和原子增强类]

文章目录

  • 1、CAS的简介
    • 1.1、什么是CAS
    • 1.2、使用CAS的前后对比
    • 1.3、CAS如何做到不加锁的情况,保证数据的一致性
    • 1.4、什么是Unsafe类
    • 1.5、CAS方法参数详解
    • 1.6、CAS的原理
    • 1.7、 CAS的缺点
  • 2、原子操作
    • 2.1、基本类型原子类
    • 2.2、数据类型原子类
    • 2.3、引用类型原子类
    • 2.4、对象的属性修改原子类
      • 2.4.1、它能帮我们解决什么问题
      • 2.4.2、使用要求
    • 2.5、原子操作增强类(jdk1.8才有)
  • 3、LongAdder效率这么快(源码分析篇)
    • 3.1、几个比较重要的成员变量以及方法
    • 3.2、LongAdder为什么这么快
    • 3.3、源码解析

1、CAS的简介

1.1、什么是CAS

CAS(Compare And Swap)的缩写,中文翻译成比较并交换,实现并发算法时常用到一种技术;他包含了3个操作数 ----- 内存位置,
,预期原值,更新值。执行CAS操作的时候,将内存位置的值与原值进行比较

  • 如果相匹配,那么处理器会自动将该位置的值更新为新值
  • 如果不匹配,处理不做任何操作,多个线程同时执行CAS操作,只有1个会成功

1.2、使用CAS的前后对比

没有CAS的时候, 我们利用sync和voliate保证符合操作的原子性
JUC并发编程高级篇第三章之CAS[Unsafe和原子增强类]
用了原子操作类后之后的操作,保证了i++的原子性,没有加入sync重量级别的锁
JUC并发编程高级篇第三章之CAS[Unsafe和原子增强类]

1.3、CAS如何做到不加锁的情况,保证数据的一致性

  • CAS是JDK提供的非阻塞原子性操作,它通过硬件保证了比较-更新的原子性。
  • 它是非阻塞的且自身具有原子性,也就是说这玩意效率更高且通过硬件保证,说明这玩意更可靠。
  • CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe 提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。
  • 执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行cas操作,也就是说CAS的原子性实际上是CPU实现独占的,比起用synchronized重量级锁,这里的排他时间要短很多,
  • 所以在多线程情况下性能会比较好。

1.4、什么是Unsafe类

  • Unsafe是CAS的核心类,由于java方法无法直接访问底层系统,需要通过本地方法来访问,Unsafe相当于是一个后门,基于该类可以直接操作特定内存你的数据.Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接去操作内存,因为java中Cas操作的执行依赖于Unsafe方法
  • CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用诒范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说 CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。

总结:CAS其实是Unsafe提供的一个方法,并且CAS是系统原语,本身就有执行过程不被中断的特性,天生就有保护原子性的特性

1.5、CAS方法参数详解

JUC并发编程高级篇第三章之CAS[Unsafe和原子增强类]

/
var1: 表示要操作的对象
var2:要操作对象属性地址偏移量
var3:表示需要修改数据的期望值
var4:需要修改为的新值
/boolean compareAndSwapObject(Object var1,long var2,Object var3,Object var4)

1.6、CAS的原理

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

  • 1 AtomicInteger里面的value原始值为,即主内存中AtomiclInteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存。
  • 2线程A通过getIntVolatile(var1, var2)拿到value值3,这时线程A被挂起
  • 3线程B也通过getIntVolatile(var1, var2)方法获取到value值3,此时刚好线程B没有被挂起并执行compareAndSwaplnt方法比较内存值也为3,成功修改内存值为4,线程B打完收工,一切OK。
  • 4这时线租A恢复,执行compareAndSwapInt方法比较,发现自己手里的值数字3和主内存的值数字4不一致,说明该值已经被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取重新来一遍了。
  • 5线程A重新获取valud值,因为变量value被volatile修饰,所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功。

1.7、 CAS的缺点

  • 问题1 : 因为do While 循环,所以可能循环时间长,开销比较大
    JUC并发编程高级篇第三章之CAS[Unsafe和原子增强类]
    解释:因为cas是处于do while循环中的,如果一直没有修改成果,则CPU处于空转状态;开销比较大;

  • 问题2 :引出ABA问题

问题的产生

  • AB两个线程做操作,主内存的值为1,此时他们进行拷贝,他们各自的空间的值都为1
  • A线程把主内存的值1改为2,然后又该1,
  • 此时B过来来修改至根据cas的期望值,他发现1就是他所期望的值,他认为并没有人对主内存进行修改过

上面过程A线程把数据从1->2->1 ,到了B线程读取的时候,进行比较比较他觉得这数据是没有人动过的,这是不符合CAS的原理的.他只管开头和结尾,不关心中心的内容,这是不对的

如何解决ABA问题

AtomicStampedReference[关心改了多少次,参考下一小节,原子类]

2、原子操作类

原子操作类,有很多这里主要把他们分类成如下几类 ,下面的代码示例中,会从中抽出来最经典的进行讲解

2.1、基本类型原子类

  • AtomicInteger (讲解案例)

public class TestMain {static AtomicInteger atomicInteger =   new AtomicInteger(0);public static void main(String[] args) throws InterruptedException {for (int i = 0; i < 10; i++) {new Thread(()->{for (int j = 0; j < 1000; j++) {add();}},"t1").start();}
//          Thread.sleep(2000);System.out.println("获取到的值是:"+atomicInteger.get());}public static void add(){atomicInteger.getAndIncrement();}
}

虽然使用了atomicInteger,但是输出结果并不是10000;

原因: 还没有等t1 线程计算完成的时候,就已经主线程就已经获取结果了, 此时我们可以使用CountDownLatch ,让主线程等待子线程结束完毕,在运行

public class TestMain {static AtomicInteger atomicInteger = new AtomicInteger(0);static CountDownLatch countDownLatch = new CountDownLatch(10);public static void main(String[] args) throws InterruptedException {for (int i = 0; i < 10; i++) {new Thread(() -> {for (int j = 0; j < 1000; j++) {add();}countDownLatch.countDown();}, "t1").start();}
//          Thread.sleep(2000);countDownLatch.await();System.out.println("获取到的值是:" + atomicInteger.get());}public static void add() {atomicInteger.getAndIncrement();}
}
  • AtomicBoolean
  • AtomicBoolean

2.2、数据类型原子类

  • AtomicIntegerArray(讲解案例)
        AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(new int[5]);//对下标为0的位置 ,增加100atomicIntegerArray.addAndGet(0,100);//对下标为1的元素 ,加1atomicIntegerArray.getAndIncrement(1);System.out.println(atomicIntegerArray.get(0));System.out.println(atomicIntegerArray.get(1));输出结果
100
1
  • AtomicLongArray
  • AtomicReferenceArray

2.3、引用类型原子类

  • AtomicReference
  • AtomicStampedReference[修改过几次,利用版本号的机制],参考ABA问题
  • AtomicMarkableReference[有没有修改过]
public class TestMain {static AtomicMarkableReference<Integer> atomicMarkableReference = new AtomicMarkableReference<>(100, false);public static void main(String[] args) throws InterruptedException {new Thread(()->{boolean marked = atomicMarkableReference.isMarked();System.out.println(Thread.currentThread().getName()+"获取的标志位为"+marked);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}boolean compareAndSet = atomicMarkableReference.compareAndSet(100, 1000, marked, !marked);System.out.println(Thread.currentThread().getName()+"修改为1000,是否成功"+compareAndSet);},"t1").start();new Thread(()->{boolean marked = atomicMarkableReference.isMarked();System.out.println(Thread.currentThread().getName()+"获取的标志位为"+marked);boolean compareAndSet = atomicMarkableReference.compareAndSet(100, 2000, marked, !marked);System.out.println(Thread.currentThread().getName()+"修改为2000,是否成功"+compareAndSet);},"t2").start();Thread.sleep(2000);System.out.println("主线程获取的值为"+atomicMarkableReference.getReference());}}输出结果
t1获取的标志位为false
t2获取的标志位为false
t2修改为2000,是否成功true
t1修改为1000,是否成功false
主线程获取的值为2000

2.4、对象的属性修改原子类

2.4.1、它能帮我们解决什么问题

作用: 以一种线程安全的方式操作非线程安全对象内的某些字段
没有使用前遇到的问题

class Book {private Integer id;private String name;public synchronized void add(){id++;}
}

在之前我们只是想修改1个id值,却直接加了synchronized; synchronized锁的是一个对象? 有没有什么办法只锁Id呢?

2.4.2、使用要求

  • 更新的对象必须使用public volatile修饰

  • 因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法NewUpdater()创建一个更新器,并且需要设置想要更新的类和属性

  • AtomicIntegerFieldUpdater

  • AtomicLongFieldUpdater

  • AtomicReferenceFieldUpdater

2.5、原子操作增强类(jdk1.8才有)

  • DoubleAccumulator
  • DoubleAdder
  • LongAccumulator(提供了自定义的函数操作)
//这个0就是x,这个1就是yLongAccumulator longAccumulator = new LongAccumulator((x, y) -> x + y, 0);//0+1longAccumulator.accumulate(1);//1+2longAccumulator.accumulate(2);System.out.println(longAccumulator.get());输出结果:3
  • LongAdder(只能用来计算加法,且之能从零开始计算)
LongAdder longAdder = new LongAdder();longAdder.increment();longAdder.increment();longAdder.increment();longAdder.increment();System.out.println(longAdder.sum());输出结果 4

LongAdder和和AtomicInterget的性能对比效率对比

package com.tvu.interruput;//需求:50个线程,每个线程100W次,总点赞数出来;import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAccumulator;
import java.util.concurrent.atomic.LongAdder;class Reader {Long number = 0L;/sync的效率/public synchronized void syncAddClick() {number++;}public Long getSyncNumber() {return number;}/Atomic的效率/AtomicLong atomicLong = new AtomicLong();public void atomicAddClick() {atomicLong.getAndAdd(1);}public Long atomicGetClick() {return atomicLong.get();}/LongAdder的效率/LongAdder longAdder = new LongAdder();public void adderClick() {longAdder.increment();}public Long getAddNumber() {return longAdder.sum();}/LongAccumulator的效率/LongAccumulator longAccumulator = new LongAccumulator((x, y) -> x + y, 0);public void accMulatorClick() {longAccumulator.accumulate(1);}public long getAccNumber() {return longAccumulator.get();}}public class TestMain {public static final int _1W = 10000;public static final int threadNumber = 50;Reader reader = new Reader();static CountDownLatch syncCountDownLatch = new CountDownLatch(threadNumber);public static void main(String[] args) throws InterruptedException {Reader reader = new Reader();long startTime = System.currentTimeMillis();for (int i = 0; i < threadNumber; i++) {new Thread(() -> {for (int j = 0; j < _1W * 1000; j++) {reader.accMulatorClick();}syncCountDownLatch.countDown();}, "t" + i).start();}syncCountDownLatch.await();long endTime = System.currentTimeMillis();//sync耗时时间为12395	 输出结果为500000000//System.out.println("sync耗时时间为" + (endTime - startTime)+"\\t 输出结果为"+reader.getSyncNumber());//atomic耗时时间为7988	 输出结果为500000000//System.out.println("atomic耗时时间为" + (endTime - startTime)+"\\t 输出结果为"+reader.atomicGetClick());//AddLong耗时时间为599	 输出结果为500000000//System.out.println("AddLong耗时时间为" + (endTime - startTime)+"\\t 输出结果为"+reader.getAddNumber());// AccMulator耗时时间为642	 输出结果为500000000System.out.println("AccMulator耗时时间为" + (endTime - startTime)+"\\t 输出结果为"+reader.getAccNumber());}
}

此时我们不禁好奇,为什么LongAdder效率这么快呢?(详情参考LongAdder源码解析)

3、LongAdder效率这么快(源码分析篇)

3.1、几个比较重要的成员变量以及方法

Striped64(他是LongAdder的父类),他主要包含了如下几个比较重要的属性和方法

  • Cell[] cells 数组,为2的幂,方便以后位运算
  • base:类似于AtomicLong中全局的value值。在没有竞争情况下数据直接累加到base上,或者cells扩容时,也需要将数据写入到base上
  • cellsBusy:初始化cells或者扩容cells需要获取锁,0:表示无锁状态 1:表示其他线程已经持有了锁
  • collide:表示扩容意向,false一定不会扩容,true可能会扩容。
  • casCellsBusy():通过CAS操作修改cellsBusy的值,CAS成功代表获取锁,返回true
  • NCPU:当前计算机CPU数量,Cell数组扩容时会使用到
  • getProbe():获取当前线程的hash值
  • advanceProbe():重置当前线程的hash值

3.2、LongAdder为什么这么快

  • AtomicInteger 慢的原因: 当线程比较多的时候,利用cas,此时空转的线程就会增多,系统cpu就会有负担
  • 利用Cell[]数组分散热点,将value分散到不同的Cell数组中,不同线程会命中到不同的槽位中,各个线程只对自己的槽中那个值进行cas操作,这样热点就分散了,冲突的概率就减少许多了;如果想要获取真正的Long值,只要将各个槽的变量值累加返回即可

3.3、源码解析

JUC并发编程高级篇第三章之CAS[Unsafe和原子增强类]

第一次进longAccumulate: 数组的初始化

  1. 如果线程竞争不激烈,则直接在base基础上进行cas操作
    JUC并发编程高级篇第三章之CAS[Unsafe和原子增强类]
  2. 如果线程竞争不激烈,则直接在base基础上进行cas操作
  3. 初始化阶段,创建2个cell数组,进行赋值
    JUC并发编程高级篇第三章之CAS[Unsafe和原子增强类]

第二次进来,数组的赋值

  1. 确定槽位,进行cas赋值
    JUC并发编程高级篇第三章之CAS[Unsafe和原子增强类]
    2.这里是直接对目前的2个槽位进行赋值,没有进longAccumulate

第三次进来,进longAccumulate,需要根据Cell的状态进入不用的if代码块
JUC并发编程高级篇第三章之CAS[Unsafe和原子增强类]

进来的前提 : 目前的有了2个cell槽位,依旧竞争激烈
JUC并发编程高级篇第三章之CAS[Unsafe和原子增强类]

此时会根据不同的cell状态,进入到不同的代码分支逻辑模块; 我们先看Cell[]数组已经初始化的情况

状态1: 如果Cell[]数组已经初始化

  • 分支1 有槽位,但是还没有值,进行赋值
    JUC并发编程高级篇第三章之CAS[Unsafe和原子增强类]
  • 分支2 槽位cas修改失败,重新抢占
    JUC并发编程高级篇第三章之CAS[Unsafe和原子增强类]
    分支3: 有槽位,且有值,直接进行修改
    JUC并发编程高级篇第三章之CAS[Unsafe和原子增强类]
    分支4:如果槽位大于cpu的数量,则不扩容
    JUC并发编程高级篇第三章之CAS[Unsafe和原子增强类]分支5:新建一个cell数组,进行扩容,迁移
    JUC并发编程高级篇第三章之CAS[Unsafe和原子增强类]
    状态2:Cell[]数组未初始化[首次新建]
    JUC并发编程高级篇第三章之CAS[Unsafe和原子增强类]
    状态3:Cell[]数组正在初始化
    如果多个线程进行casCellsBusy 修改锁状态失败,则会进入到这个分支;cell初始化不了,就把值加给base进行累加
    JUC并发编程高级篇第三章之CAS[Unsafe和原子增强类]