JUC框架之——CAS及Atomic类

Posted 无扬的博客

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JUC框架之——CAS及Atomic类相关的知识,希望对你有一定的参考价值。

目录

  • 0、前言

  • 1、案例演练

  • 2、Atomic中CAS的应用

  • 3、CAS缺陷及解决方案

0、前言

多线程的同步问题,可以通过加锁来解决,尽管内置锁(synchronized)和显示锁(Lock)能解决问题,但是有些场景下可以使用更加轻量级的“锁”方式来解决问题,CAS(Compare And Swap)即是这样一种实现。

CAS是一种无锁算法,核心思想是 如果V的值等于期望值A,那么将V的值更新为B,否则不操作。CAS是CPU指令,属于硬件支持,保证原子性。

1、案例演练

假如需要一个全局的计数器,每执行一次某操作,该计数器就加一,下面分别使用 volatile变量和 AtomicInteger来实现该计数器

 
   
   
 
  1. public class CasTest {

  2.    private static AtomicInteger atomicCounter = new AtomicInteger(1);

  3.    private static volatile int volatileCounter = 1;

  4.    @Test

  5.    public void test() throws Exception{

  6.        CountDownLatch threadReady = new CountDownLatch(1);

  7.        CountDownLatch mainResult = new CountDownLatch(10);

  8.        for (int i=1; i<=10; i++) {

  9.            new Thread(() -> {

  10.                try {

  11.                    threadReady.await();

  12.                } catch (InterruptedException e) {

  13.                }

  14.                for (int j=1; j<=100; j++) {

  15.                    atomicCounter.incrementAndGet();

  16.                    volatileCounter++;

  17.                }

  18.                // 通知主线程查看计数器的值

  19.                mainResult.countDown();

  20.            }).start();

  21.        }

  22.        threadReady.countDown();

  23.        mainResult.await();

  24.        System.out.println("atomic计数器运行结果: "+atomicCounter.get());

  25.        System.out.println("volatile计数器运行结果: "+volatileCounter);

  26.    }

  27. }

运行结果(不一定必现,多运行几次):

 
   
   
 
  1. atomic计数器运行结果: 1001

  2. volatile计数器运行结果: 999

上面代码段可以得出的结论:

1、 volatile虽然可以保证可见性,但是无法保证原子性(复合操作)

2、 AtomicIntegerincrementAndGet()方法可以保证原子性

2、Atomic中CAS的应用

2.1 AtomicInteger中CAS的应用

看了上面案例,那么 AtomicIntegerincrementAndGet()是如何保证原子性的呢?首先 AtomicInteger维护了3个属性:

 
   
   
 
  1.    private static final Unsafe unsafe = Unsafe.getUnsafe();

  2.    private static final long valueOffset;

  3.    static {

  4.        try {

  5.            valueOffset = unsafe.objectFieldOffset

  6.                (AtomicInteger.class.getDeclaredField("value"));

  7.        } catch (Exception ex) { throw new Error(ex); }

  8.    }

  9.    private volatile int value;

1、 unsafe对象:

该对象是CAS的核心,java代码是通过 Unsafe来实现CAS指令操作, Unsafe中的方法几乎全都是 native修饰,底层调用的是C/C++代码.

2、 valueOffset

3、 value

volatile变量用来存储当前的值,对该值直接进行 get()set(intvalue)操作时,无需使用 unsafe的CAS方法,因为单指令赋值操作本身就具有原子性。

接下来看 AtomicInteger.incrementAndGet()的实现代码:

 
   
   
 
  1.    public final int incrementAndGet() {

  2.        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;

  3.    }

然后再看 unsafe.getAndAddInt方法的实现代码:

 
   
   
 
  1.    public final int getAndAddInt(Object var1, long var2, int var4) {

  2.        int var5;

  3.        do {

  4.            var5 = this.getIntVolatile(var1, var2);

  5.        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

  6.        return var5;

  7.    }

当CAS操作失败时(有其它线程已经修改了原值V),会通过

var5=this.getIntVolatile(var1,var2);来获取最新的值。

CAS的操作方法: this.compareAndSwapInt(var1,var2,var5,var5+var4),会调用Unsafe的

compareAndSwapInt(Objectvar1,longvar2,intvar4,intvar5)方法,该方法的4个参数含义分别是:

1、 Objectvar1: AtomicInteger对象本身

3、 intvar4:期望值,可以理解为当前CAS操作时认为的旧值

4、 intvar5:设定值,CPU指令操作时如果实际值和期望值相同,则把旧值替换(swap)为设定值

如果每次CAS都失败,则在while中会一直循环重试,直到成功,这个循环重试的过程也叫 自旋。有时候容易把CAS操作和自旋划上等号,底层的CAS返回就是一个boolean类型,代码层面根据返回的结果来进行循环重试,才构成了自旋。 AtomicInteger.compareAndSet(intexpect,intupdate)方法就不涉及自旋,一次CAS操作之后就返回,成功就返回true,失败就返回false。

2.2 其余Atomic类

在juc的atomic包下面还有很多Atomic开头的类,使用方法的实现和 AtomicInteger有很多相似的地方,可以对比起来研究,其核心都是一个对比、修改的过程。

另外java的CAS操作都是通过 Unsafe类来进行二次封装的,不建议直接获取 Unsafe类,同时 Unsafe类的 publicstaticUnsafegetUnsafe()方法只对jdk的类开放,自定义的类中调用该方法是会抛异常的,如果实在要获取该类的实例,那只能通过反射。

3、CAS缺陷及解决方案

3.1 ABA问题

什么是ABA问题不在此赘述,不是说ABA问题一出现就一定需要解决,只不过有些业务场景下ABA问题是不能容忍的,这些场景下ABA问题才需要解决。

atomic包下面为解决ABA问题提供了一个 AtomicStampedReference<V>类,其维护了一个Pair属性,Pair中保存了 reference值和该值对应的版本号:

 
   
   
 
  1.    private static class Pair<T> {

  2.        final T reference;

  3.        final int stamp;

  4.        private Pair(T reference, int stamp) {

  5.            this.reference = reference;

  6.            this.stamp = stamp;

  7.        }

  8.        static <T> Pair<T> of(T reference, int stamp) {

  9.            return new Pair<T>(reference, stamp);

  10.        }

  11.    }

  12.    private volatile Pair<V> pair;

AtomicStampedReference<V>类将真正需要的值( <V>) reference进行保存的同时还保存一个版本号 stamp,在进行 compareAndSet的时候,不仅带上期望值 reference,还带上该期望值对应的版本号 stamp,使用实例如下:

 
   
   
 
  1.    AtomicStampedReference<String> atomicStampedReference =

  2.        new AtomicStampedReference<>("stampedReference", 1);

  3.    String oldValue = atomicStampedReference.getReference();

  4.    int oldStamp = atomicStampedReference.getStamp();

  5.    atomicStampedReference.compareAndSet(oldValue, "new stampedReference", oldStamp, oldStamp+1);

如果需要自旋的设置值,比如就是要把值设置成 "new stampedReference"

 
   
   
 
  1.    AtomicStampedReference<String> atomicStampedReference =

  2.        new AtomicStampedReference<>("stampedReference", 1);

  3.    String oldValue = atomicStampedReference.getReference();

  4.    int oldStamp = atomicStampedReference.getStamp();

  5.    while (!atomicStampedReference.compareAndSet(oldValue, "new stampedReference", oldStamp, oldStamp+1)) {

  6.        oldStamp = atomicStampedReference.getStamp();

  7.    }

另外大多数Atomic类都有一个 weakCompareAndSet方法,该方法从doc上看多了一行注释:

May fail spuriously and does not provide ordering guarantees, so is only rarely an appropriate alternative to {@code compareAndSet}.

查阅了一些资料,其实这里的大意应该是说 weakCompareAndSet操作仅保留了volatile自身变量的特性,而去除了happens-before规则带来的内存语义。也就是说,weakCompareAndSet无法保证处理操作目标的volatile变量外的其他变量的执行顺序( 编译器和处理器为了优化程序性能而对指令序列进行重新排序 ),同时也无法保证这些变量的可见性(此处见参考链接3)。不过在jdk8中两者实现一模一样,没有任何差别,所以应该是 weakCompareAndSet先占了坑位,但是还没有具体实现。

3.2 自旋操作时循环时间太长

在有些场景下面,可能会有自旋的循环时间次数很多,占用大量CPU资源,可以在掉用cas操作的地方加上自旋最大次数。例如在 SynchronousQueue类的 ObjectawaitFulfill(QNodes,E e,booleantimed,longnanos)方法中,对自旋的次数就做了限制。

3.3 仅能保证一个共享变量原子操作

对于单个共享变量,我们可以使用CAS循环的方式来保证原子操作,但是对多个共享变量操作时,CAS循环就无法保证操作的原子性,这个时候一般需要加锁操作,或者是把多个变量整合成一个变量。

参考链接

1、https://www.cnblogs.com/Mainz/p/3546347.html

2、http://cmsblogs.com/?p=2235

3、https://www.jianshu.com/p/55a66113bc54

4、《Java特种兵》


以上是关于JUC框架之——CAS及Atomic类的主要内容,如果未能解决你的问题,请参考以下文章

面试必会必知:线程池架构浅析

Java中CAS 基本实现原理

ava多线程系列 JUC原子类 CAS及原子类

JUC - 多线程之 CAS和原子类

Java多线程系列---“JUC原子类”01之 原子类的实现(CAS算法)

JUC之原子性操作