JUC 原子操作封装类

it2022-05-05  65

文章目录

1 什么是 JUC 原子操作封装类2 JUCA 的应用场景3 锁与 JUCA 的选择问题4 为什么使用 JUCA5 JUCA 源码分析5.1 原子操作类总纲5.2 Atomic 类5.3 AtomicArray 类5.4 AtomicFieldUpdater 类5.5 AtomicReference 类5.6 多操作单元 Atomic 类5.7 原子操作类总结 6 CAS 浅析6.1 什么是 CAS6.2 CAS 的应用场景6.3 为什么要用 CAS6.4 CAS 原理介绍6.5 CAS 存在的问题6.6 ABA 问题 参考

1 什么是 JUC 原子操作封装类

原子操作封装类是 JUCA(java.util.concurrent.atomic)包下的类的总称都是基于 CAS(compare and swap) 实现的它可以将单个变量的赋值操作封装成原子性操作

2 JUCA 的应用场景

多线程下,不使用锁,实现单个变量的赋值操作

3 锁与 JUCA 的选择问题

极其高的并发下,可能有许多线程一直做循环 CAS,导致 CPU 无效使用率太高,进一步导致赋值操作效率越来越低 一个线程设置完,其它线程如果都读取到旧值,后面争取 CPU 和比较操作都是无意义的消耗,并且一个线程可能有好多次的这种无意义的消耗而如果使用锁,就不存在读取旧值、比较操作的 CPU 消耗,只有加锁/释放锁的消耗,并且只有固定次数的锁消耗 上面说的极其高也没有一个具体的说法,并且随着 synchornized 锁的不断优化,原生锁的性能也会越来越高只是 JUCA 包下的类使用会更简洁,并不一定性能就高具体场景可以综合测试最后做出选择,一般没必要纠结,最赋值操作就简单的选择 JUCA

4 为什么使用 JUCA

提供了多线程下单个变量赋值的原子性操作使用简单

5 JUCA 源码分析

5.1 原子操作类总纲

5.2 Atomic 类

Atomic 类有三种子类,分别代表原子更新的布尔类型、整型、长整型AtomicBoolean 方法解析:6个方法 内部通过 volatile int value 属性的 CAS 操作实现原子性操作可见 Boolean 属性最终转换为 int 属性来处理了 AtomicBoolean ab = new AtomicBoolean(true); ab.compareAndSet(true,false); //比较然后赋值,如果旧值为 true,就设置为 false,设置成功才返回 true,否则返回 false,单个 CAS 就结束 ab.weakCompareAndSet(true,false); //目前和 compareAndSet 方法一模一样 ab.get(); //获取最新的值(因为有 volatile 修饰) ab.getAndSet(true); //循环 CAS 获取最新的值,并赋为 true ab.lazySet(false); //内部调用unsafe.putOrderedInt() 方法,这个是普通的设置int,值未必立刻回写到主存 ab.set(true); //立即set,因为内部的 value 属性被 volatile 修饰 AtomicInteger 方法解析:16个方法 内部通过 volatile int value 属性的 CAS 操作实现原子性操作相比AtomicBoolean类,多了很多赋值方法 AtomicInteger ai = new AtomicInteger(0); ai.getAndIncrement(); //获取旧值,并加1,内部 CAS 循环加1,直到成功 ai.incrementAndGet(); //加1,并获取新值,内部 CAS 循环加1,直到成功 ai.getAndDecrement(); //获取旧值,并减1,内部 CAS 循环减1,直到成功 ai.decrementAndGet(); //减1,并获取新值,内部 CAS 循环减1,直到成功 ai.getAndAdd(2); //获取旧值,并加2,内部 CAS 循环加2,直到成功 ai.addAndGet(2); //加2,并获取新值,内部 CAS 循环加2,直到成功 ai.getAndAccumulate(6,Math::max); //循环 CAS 设置 二元运算后的值。二值运算的第一位是获取到的最新值,第二位是设置的值(6) ai.accumulateAndGet(4,(x,y)->{ //和上面的区别只是,最后返回的值是 二元运算后的值 return x+y; }); ai.getAndUpdate(Math::abs); //循环 CAS 设置 一元运算后的值,获取的是旧值 ai.updateAndGet((x)->{ //循环 CAS 设置 一元运算后的值,获取的是新值 return x*x; }); ai.get(); //获取最新的值(因为有 volatile 修饰) ai.getAndSet(2); //循环 CAS 获取最新的值,并设置属性为2 ai.set(3); //设置属性为2 ai.compareAndSet(1,2); //如果属性为1,则设置为2。成功返回 true。否则返回 false ai.lazySet(3); //ab.lazySet(false) ai.weakCompareAndSet(3,3); //目前和 compareAndSet() 方法一样 AtomicLong 方法解析:和 AtomicInteger 方法一模一样,只是 value 的属性从 int 变成了 long

5.3 AtomicArray 类

实现数组中元素的原子性更新操作

AtomicArray 类内部维护一个 final 修饰的 array 数组

array 数组并没有被 volatile 修饰即使数组被 volatile 修饰,只是数组的引用可见,数组的内部元素并不可见通过 unsafe.getIntVolatile(…) 方法保证 array 数组中元素的可见性的

其余的方法和 Atomic 类是一样的,只是所有方法都多了一个 index 属性,表明操作数组的哪一位

AtomicReferenceArray:原子更新的是引用类型的引用(即只能引用重新指向),而不是引用类型内部的值

5.4 AtomicFieldUpdater 类

实现目标类中的字段的原子性更新操作

AtomicFieldUpdater 类内部维护一个 final 修饰的 offset 变量(通过反射得到类中 filed 属性对应的偏移量)

通过偏移量,使用 CAS 来实现原子操作,操作和 Atomic 类一样

目标类中的对应字段必须被 volatile 修饰,且不能被 private 修饰

//这个 Target 是 age 字段必须被 volatile int 修饰,且不能被 private 修饰 AtomicIntegerFieldUpdater AIFU = AtomicIntegerFieldUpdater.newUpdater(Target.class,"age"); Target target = new Target("wk",24); aifu.accumulateAndGet(target,56,(x,y)->{ return x+y; }); System.out.println(aiuf.getAge()); //这个 Target 是 time 字段必须被 volatile long 修饰,且不能被 private 修饰 AtomicLongFieldUpdater ALFU = AtomicLongFieldUpdater.newUpdater(Target.class,"time"); //这个 Target 是 man 的字段必须是 man.class 类,并且 Target 中的该字段必须被 volatile 修饰,并且不能被 private 修饰 AtomicReferenceFieldUpdater ARFU = AtomicReferenceFieldUpdater.newUpdater(Target.class,man.class,"man");

5.5 AtomicReference 类

AtomicReference 方法解析:10个方法 和 AtomicInteger 类似,只是这个原子更新的是引用类型的引用地址 AtomicReference ar = new AtomicReference(); System.out.println(ar.get());//null ar.compareAndSet(null,"string"); System.out.println(ar.get());//string,从 null 变成了 string //aiuf 是静态内部类,里面有几个基本属性 AtomicFieldUpdater.aiuf a = new AtomicFieldUpdater.aiuf("wk",22); AtomicFieldUpdater.aiuf b = new AtomicFieldUpdater.aiuf("wkk",222); AtomicReference ar1 = new AtomicReference(a); System.out.println(ar1.get());//aiuf{name='wk', age=22, man=null} ar1.compareAndSet(a,b); //注意:ar1.get() 变掉了 System.out.println(ar1.get());//aiuf{name='wkk', age=222, man=null} AtomicStampedReference 方法解析: AtomicFieldUpdater.aiuf a = new AtomicFieldUpdater.aiuf("wk",22); AtomicFieldUpdater.aiuf b = new AtomicFieldUpdater.aiuf("wkk",222); AtomicStampedReference asr = new AtomicStampedReference(a,0); boolean flag = asr.attemptStamp(a,1); //如果引用是 a,且stamp没变,将 stamp 设置为1 System.out.println(flag); asr.compareAndSet(a,b,1,2);//单个 CAS,如果是 a,1,那么设置为 b,2 int[] stampHolder = new int[1];//只是用来存储 stamp; AtomicFieldUpdater.aiuf aa = (AtomicFieldUpdater.aiuf)asr.get(stampHolder);//这个 stampHolder 是get() 用来回调的。这样就相当于返回了 两个参数,引用和 stamp System.out.println(aa); asr.getReference();//获取引用,因为Pair类使用 volatile 修饰,且使用的是引用的赋值,所以 Pair 中的属性也是可见的 asr.getStamp();//同理 asr.set(a,2); //pair 是可见的,且是引用赋值,所以 引用的赋值时原子的 asr.weakCompareAndSet(a,b,2,3);//内部调用compareAndSet

5.6 多操作单元 Atomic 类

多操作单元类: 其他 Atomic 类中更新的只是一个变量多操作变量,更新的是多个变量,最后取的时候再合并这多个变量这样就提高了 Atomic 类的更新速度(多个线程可以同时更新不同的单元)但是读取时,需要综合所有单元的值,所以适合写多读少的场景 DoubleAccumulator 方法解析 这个类操作的是 double 类型的数据,有4个方法源码过于难理解,并且获取的值还不准确,可能用于不精确的多更新操作吧 DoubleAccumulator da = new DoubleAccumulator((x,y)->{ return x*y; },1.2); //通过设定的值 1.2 和 3 做 二元操作符运算,得到新的设定值 da.accumulate(3); //返回当前值。返回的值不是原子快照;在没有并发更新的情况下调用会返回准确的结果,但在计算值时发生的并发更新可能不会合并!-----------返回结果不准确!--------- da.get(); //重置 da 至原始状态,相对于重新 new 一个 da,但是只有没有并发更新的时候才有效,不然不准确,,,,, da.reset(); //顾名思义 da.getThenReset(); LongAccumulator 方法解析 和 DoubleAccumulator 的区别仅仅在于返回值的类型变成了 long DoubleAdder 方法解析 可以当作 DoubleAccumulator 的子类,它的 accumulator 为 二元相加运算符值也不准确 LongAdder 方法解析 可以当作 LongAccumulator 的子类,它的 accumulator 为 二元相加运算符值也不准确

5.7 原子操作类总结

普通 Atomic 类,是通过 volatile 修饰 value 属性,来保证可见性的AtomicArray类,是通过 getXXXVolatile()、putXXXVolatile() 方法,来保证数组内部元素的可见性的所有的原子类操作,最后大多会调用 unsafe.compareAndSwapXXX(this,offset,old,new) 方法 XXX 的种类:Int、Long、Objectoffset:偏移量(属性具体的地址)old:预期的值new:要更新成的值

6 CAS 浅析

6.1 什么是 CAS

CAS 是 compare and swap 的简称,意为:比较并替换执行 CAS 操作时,当且仅当内存地址 V 的值与预期值 A 相同时,将内存地址 V 的值修改为 B,否则就什么都不做。整个比较过程是原子性操作

6.2 CAS 的应用场景

大量用于 JUCA 包中,来做原子性更新变量的操作对单个变量做原子性更新的操作

6.3 为什么要用 CAS

通过 CAS 可以实现无锁同步,避免锁带了的消耗(当然也存在一些问题)

6.4 CAS 原理介绍

通过 AtomicInteger 类源码来理解 CAS AtomicInteger ai = new AtomicInteger(0); ai.getAndIncrement();//获取并是 ai 值加 1 public final int getAndIncrement() { //this 的作用是传入 ai 对象的地址、valueOffset 的作用是传入 ai 对象中 value 变量的地址偏移量、1 为要增加的值 return unsafe.getAndAddInt(this, valueOffset, 1); } public final int getAndAddInt(Object var1, long var2, int var4) { int var5; //这里使用了 循环 CAS 的方式,一直到 CAS 成功才返回! do { //通过 ai 对象的地址和地址偏移量(value 变量相对于对象地址的偏移量),得到当前 value 变量的实际地址的值 var5 = this.getIntVolatile(var1, var2); //比较 (var1,var2)、var5 -- 如果 var1 地址对应的 var2 偏移量地址的值 和 var5 相同,就将 value 变量所在地址的值替换为 var4+ var5,即原始的值+1 } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; } public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5); 以上代码是循环 CAS 操作,单纯的 CAS 只有两个步骤 步骤1:获取偏移量地址的值,作为预期值步骤2:比较偏移量地址的值(这时该地址的值可能已被别的线程修改了)和预期值,如果相等将该地址的值设置为 更新值,设置成功返回 true,否则返回 false 通过以上代码可以看出,CAS 需要 4 个参数:对象地址、偏移量、预期值、更新值如果对象地址对应的偏移量地址的值 和 预期值相同,就将这个 偏移量地址的值 设置为 更新值

6.5 CAS 存在的问题

如果要用 循环 CAS 来代替加锁操作,可能要循环很久才会成功

只能对一个变量进行操作,如果要对多个变量操作,必须要加锁

ABA 问题

6.6 ABA 问题

- 有一个变量 p = 4 - 线程1 通过步骤1读取了 p 的预期值 4 - 线程2 通过步骤1读取了 p 的预期值4,并通过步骤2成功将其设置成9 - 线程3 通过步骤1读取了 p 的预期值9,并通过步骤2成功将其设置成4 - 线程1 通过步骤2成功将其设置成了21 虽然这里线程1成功 CAS 了,但是我们真实意图其实线程1应该 CAS 失败的 因为步骤2成功的标准是 p 的值没有变,没有变真正含义,应该是没有变动 而 p 的值虽然最终没有变,但它变动了 因为以上语义问题导致的经典 ABA 问题 - 有一个栈,目前的元素为:A->B->C->null, A 为 top,即 top 指针指向 A - 现成我们通过 CAS 做出栈入栈操作 - 线程1 做 pop 操作,首先取到 top 指向的值 为A,并且取到 A 的下一个节点 B - 然后 线程1 准备(注意:是准备,但是没有做)做 CAS 操作,即 CAS(&top,A,B):如果top指针指向的仍然是 A 的话,则把 top 指针指向 B - 这时,线程2 做了一次完整的 pop 操作,此时栈的元素为:B->C->null - 线程2 又做了一次完整的 pop 操作,此时栈的元素为:C->null - 接着,线程3 做了一次 完整的 push(A) 操作,此时栈的元素为:A->C->null,现在 top 又指向 A 了 - 这时,线程1 终于正式 CAS(&top,A,B)了,成功执行!!!把 top 执行了 B,这样栈的元素为:B->null - 问题所在:原本的栈:A->C->null,突然变为 B->null。这时一个很严重的问题 - 最关键的是,线程1 最后的 CAS 操作不应该成功,线程1 应该重新开始 pop,最终的栈也应该是:C->null!!! java 源码模拟以上问题 public class CasStack { AtomicReference<Node> top = new AtomicReference<>();// top指针 cas无锁修改 public void push(Node node) {//入栈 Node oldTop; do { oldTop = top.get();//获取预期的 top 指针指向的值 node.next = oldTop;//并将新的节点的下一个节点指向 预期值,即头插法 } while (!top.compareAndSet(oldTop, node));// 如果现在 top 指针指向的值还是预期值的话,就将 top 指针指向 新节点 node } public Node pop() {// 出栈 Node newTop; Node oldTop; do { oldTop = top.get();//获取预期值 if (oldTop == null) {//如果没有值,就返回null return null; } newTop = oldTop.next; LockSupport.parkNanos(1000 * 1000 * 5); // 休眠指定的时间 } while (!top.compareAndSet(oldTop, newTop));//如果 top 指向的值还是预期值 oldTop ,则将 top 指向 之前取到的下一个值 oldTop.next = null;//帮助GC return oldTop;//将旧的Top作为值返回 } } class Node { public String value; public Node next; public Node(String value) { this.value = value; } } ABA 问题解决方案 一般通过额外添加一个版本号属性,比较值的同时比较版本号,如果两个都相同才 CAS 替换 public class ConcurrentStack { AtomicStampedReference<Node> top = new AtomicStampedReference<>(null, 0);//带版本号的 CAS public void push(Node node) { // 入栈 Node oldTop; int v; do { v = top.getStamp();//获取版本号 oldTop = top.getReference();//获取值 node.next = oldTop;//将新节点的下一节点指向旧节点 } while (!top.compareAndSet(oldTop, node, v, v+1)); // 关键:将内部 oldTop,v包装成了一个新对象,最后调用的还是最经典的compareAndSwapXXX() 方法,只是内部的参数是包装后的对象 } // 出栈 -- 取出栈顶 ,为了演示ABA效果, 增加一个CAS操作的延时 public Node pop() { Node newTop; Node oldTop; int v; do { v = top.getStamp(); oldTop = top.getReference(); if (oldTop == null) { //如果没有值,就返回null return null; } newTop = oldTop.next; LockSupport.parkNanos(1000 * 1000 * 5); // 休眠指定的时间 } while (!top.compareAndSet(oldTop, newTop, v, v+1));//同理 oldTop.next = null; return oldTop; } } 使用同一个测试方法 public class Test { public static void main(String[] args) throws InterruptedException { Stack stack = new Stack(); //ConcurrentStack stack = new ConcurrentStack(); stack.push(new Node("B")); //B入栈 stack.push(new Node("A")); //A入栈 Thread thread1 = new Thread(() -> { Node node = stack.pop(800); System.out.println(Thread.currentThread().getName() +" "+ node.toString()); System.out.println("done..."); }); thread1.start(); Thread thread2 = new Thread(() -> { LockSupport.parkNanos(1000 * 1000 * 300L); Node nodeA = stack.pop(0); //取出A System.out.println(Thread.currentThread().getName() +" "+ nodeA.toString()); Node nodeB = stack.pop(0); //取出B,之后B处于游离状态 System.out.println(Thread.currentThread().getName() +" "+ nodeB.toString()); stack.push(new Node("D")); //D入栈 stack.push(new Node("C")); //C入栈 stack.push(nodeA); //A入栈 System.out.println("done..."); }); thread2.start(); LockSupport.parkNanos(1000 * 1000 * 1000 * 2L); System.out.println("开始遍历Stack:"); Node node = null; while ((node = stack.pop(0))!=null){ System.out.println(node.value); } } }

参考

JDK 1.8u171


最新回复(0)