并发编程之CAS

CAS介绍

什么是 CAS

CAS(Campare And Swap,比较并交换),通常指一种原子操作:针对一个变量,首先比较它的内存值与期望值是否相同,相同就给它赋一个新值。
CAS的逻辑用伪代码描述如下:

if (value == expectedValue) {
    value = newValue;

上面的伪代码描述了一个由比较和复制的两个阶段组成的复合操作,实际上,CAS可以看成是他们的一个整合体,一个不可分割的原子操作,由硬件层面来保障原子性。
CAS可以看做是乐观锁的一种实现,我们Java中的原子中的递增操作就通过CAS自旋实现的。
CAS是一种无锁算法,在没锁的情况下,也就是不阻塞线程的情况下,实现多线程之间的变量同步。

CAS在Java中的应用

在 Java 中,CAS 操作是由 Unsafe 类提供支持的,该类定义了三种针对不同类型变量的 CAS 操作,它们都是native方法,由Java 虚拟机提供具体实现

public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

这里我们以compareAndSwapInt方法为例,该方法接收四个参数,分别表示:对象实例,内存地址便宜量,字段期望值,字段新值。该方法会针对指定对象实例中的相应偏移量的字段进行CAS操作。
代码示例:

public class CASDemo {

    public static void main(String[] args) {
        Entity entity = new Entity();
        Unsafe unsafe = UnsafeFactory.getUnsafe();

      
        // 获取Entity对象字段x的地址偏移量
        long offset = UnsafeFactory.getFieldOffset(unsafe, Entity.class, "x");
        System.out.println(offset);

        boolean successful;
        // 4个参数分别是:对象实例、字段的内存偏移量、字段期望值、字段更新值
        successful = unsafe.compareAndSwapInt(entity, offset, 0, 3);
        System.out.println(successful + "\t" + entity.x);

        successful = unsafe.compareAndSwapInt(entity, offset, 3, 5);
        System.out.println(successful + "\t" + entity.x);

        successful = unsafe.compareAndSwapInt(entity, offset, 3, 8);
        System.out.println(successful + "\t" + entity.x);

    }
    
    
}

class Entity{
    int x;
}


上述代码示例运行结果:

12
true	3
true	5
false	5

针对Entity的x属性进行3次CAS操作,分别尝试将x从0改成3,从3改成5,从3改成8。最后一次执行前,x的值为5,期望值3和内存值不相等,执行失败,返回false。

UnsafeFactory.java

public class UnsafeFactory {

    /**
     * 通过反射获取Unsafe对象
     * @return
     */
    public static Unsafe getUnsafe() {
        try {
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            return (Unsafe)theUnsafe.get(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 获取指定字段偏移量
     * @param unsafe
     * @param clazz
     * @param fieldName
     * @return
     */
    public static long getFieldOffset(Unsafe unsafe, Class clazz, String fieldName) {
        try {
            return unsafe.objectFieldOffset(clazz.getDeclaredField(fieldName));
        } catch (NoSuchFieldException e) {
            throw new Error(e);
        }
    }
}

CAS缺陷

CAS虽然高效地解决了原子操作,但是也存在着一些缺陷,主要表现在三个方面:

  1. 自选CAS长时间不成功,给CPU带来非常大的开销
  2. 只能保证一个共享变量的原子操作
  3. ABA问题

ABA问题及其解决方案

CAS算法实现是,从内存中读取某时刻的数据,下一刻比较并替换,在这之间就会存在一个时间差,就会发生ABA问题。

什么是ABA问题

当多个线程对一个原子类进行操作时,某个线程在短时间内将原子类的值A修改为B,立马又将其修改回A,其他线程是无法感知的,会任务原子类没有发生过变化,CAS操作还是能够成功。
在这里插入图片描述

代码演示:

@Slf4j
public class ABADemo {

    public static void main(String[] args) {
        // 原值10
        AtomicInteger atomicInteger = new AtomicInteger(10);
        
        new Thread(()->{
            int value = atomicInteger.get();
            log.debug("ThreadA read value: " + value);

            // 阻塞1s
            LockSupport.parkNanos(1000000000L);
            
            // ThreadA 通过cas修改value为30
            if (atomicInteger.compareAndSet(value, 30)) {
                log.debug("ThreadA update from " + value + " to 30");
            } else {
                log.debug("ThreadA update fail!");
            }
            
        }, "ThreadA").start();

        new Thread(()->{
            int value = atomicInteger.get();
            log.debug("ThreadB read value: " + value);
            
            // ThreadB 通过cas修改value为20
            if (atomicInteger.compareAndSet(value, 20)) {
                log.debug("ThreadB update from " + value + " to 30");
                
                // 其他业务

                value = atomicInteger.get();
                // ThreadB通过CAS修改value值为10
                if (atomicInteger.compareAndSet(value, 10)) {
                    log.debug("ThreadB update from " + value + " to 10");
                }
                
            }

        }, "ThreadB").start();
    }
    
}

运行结果:

[ThreadB] DEBUG org.example.juc.automic.ABADemo - ThreadB read value: 10
[ThreadA] DEBUG org.example.juc.automic.ABADemo - ThreadA read value: 10
[ThreadB] DEBUG org.example.juc.automic.ABADemo - ThreadB update from 10 to 30
[ThreadB] DEBUG org.example.juc.automic.ABADemo - ThreadB update from 20 to 10
[ThreadA] DEBUG org.example.juc.automic.ABADemo - ThreadA update from 10 to 30

从运行结果就可以看出,ThreadA不清楚ThreadB对共享变量进行过操作,认为value没有修改过

ABA问题的解决方案

数据库中有一种乐观锁,是一种基于数据版本实现数据同步的机制,没修改一次数据,版本就会进行累加。
同样,Java中也有类似的版本解决方案:原子引用类AtomicStampedReference

public class AtomicStampedReference<V> {

    private static class Pair<T> {
        final T reference;
        final int stamp;
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
    }

    private volatile Pair<V> pair;
    
    public AtomicStampedReference(V initialRef, int initialStamp) {
        pair = Pair.of(initialRef, initialStamp);
    }
    
    ...
}

reference即我们实际存储的变量,stamp是版本,每次修改可以通过+1保证版本唯一性。这样就可以保证每次修改后的版本也会往上递增。

常用方法
传入一个int类型数组,返回我们当前内存实际存储的变量reference,并将传入的int数组第0位元素写入当前版本号

public V get(int[] stampHolder)

第一个参数期望值,第二个参数新值,第三个参数期望版本号,第四个参数为更新成功后的版本号。

public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp,int newStamp) 

代码测试:

@Slf4j
public class AtomicStampedReferenceDemo {

    public static void main(String[] args) {
        // 定义AtomicStampedReference    Pair.reference值为1, Pair.stamp为1
        AtomicStampedReference atomicStampedReference = new AtomicStampedReference(10,1);

        new Thread(()->{
            int[] stampHolder = new int[1];
            int value = (int) atomicStampedReference.get(stampHolder);
            int stamp = stampHolder[0];
            log.debug("ThreadA read value: " + value + ", stamp: " + stamp);

            // 阻塞1s
            LockSupport.parkNanos(1000000000L);

            // ThreadA 通过cas修改value为30
            if (atomicStampedReference.compareAndSet(value, 30, stamp, stamp+1)) {
                log.debug("ThreadA update from " + value + " to 30");
            } else {
                log.debug("ThreadA update fail!");
            }

        }, "ThreadA").start();

        new Thread(()->{
            int[] stampHolder = new int[1];
            int value = (int) atomicStampedReference.get(stampHolder);
            int stamp = stampHolder[0];
            log.debug("ThreadB read value: " + value + ", stamp: " + stamp);

            // ThreadB 通过cas修改value为20
            if (atomicStampedReference.compareAndSet(value, 20, stamp, stamp + 1)) {
                log.debug("ThreadB update from " + value + " to 20");

                // 其他业务

                value = (int) atomicStampedReference.get(stampHolder);
                stamp = stampHolder[0];
                log.debug("ThreadB read value: " + value+ ", stamp: " + stamp);
                // ThreadB通过CAS修改value值为10
                if (atomicStampedReference.compareAndSet(value, 10, stamp, stamp + 1)) {
                    log.debug("ThreadB update from " + value + " to 10");
                }

            }

        }, "ThreadB").start();
        
    }
    
}

运行结果:

[ThreadA] DEBUG org.example.juc.automic.AtomicStampedReferenceDemo - ThreadA read value: 10, stamp: 1
[ThreadB] DEBUG org.example.juc.automic.AtomicStampedReferenceDemo - ThreadB read value: 10, stamp: 1
[ThreadB] DEBUG org.example.juc.automic.AtomicStampedReferenceDemo - ThreadB update from 10 to 20
[ThreadB] DEBUG org.example.juc.automic.AtomicStampedReferenceDemo - ThreadB read value: 20, stamp: 2
[ThreadB] DEBUG org.example.juc.automic.AtomicStampedReferenceDemo - ThreadB update from 20 to 10
[ThreadA] DEBUG org.example.juc.automic.AtomicStampedReferenceDemo - ThreadA update fail!

ThreadA修改失败。

Atomic原子操作类介绍

在并发编程中很容易出现并发安全的问题,有一个很简单的例子就是多线程更新变量i=1,比如多个线程执行i++操作,就有可能获取不到正确的值,而这个问题,最常用的方法是通过Synchronized进行控制来达到线程安全的目的。但是由于synchronized是采用的是悲观锁策略,并不是特别高效的一种解决方案。实际上,在J.U.C下的atomic包提供了一系列的操作简单,性能高效,并能保证线程安全的类去更新基本类型变量,数组元素,引用类型以及更新对象中的字段类型。atomic包下的这些类都是采用的是乐观锁策略去原子更新数据,在java中则是使用CAS操作具体实现。

在java.util.concurrent.atomic包里提供了一组原子操作类:
基本类型:AtomicInteger、AtomicLong、AtomicBoolean;
引用类型:AtomicReference、AtomicStampedRerence、AtomicMarkableReference;
数组类型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
对象属性原子修改器:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
原子类型累加器:DoubleAccumulator、DoubleAdder、LongAccumulator、LongAdder、Striped64(jdk1.8增加的类)

原子类使用

基本类型原子类使用

以AtomicInteger为例总结常用的方法:

getAndIncrement():以原子的方式将实例中的原值加1,返回的是自增前的旧值

public final int getAndIncrement()

getAndSet(int newValue):将实例中的值更新为新值,并返回旧值

public final boolean getAndSet(boolean newValue)

incrementAndGet() :以原子的方式将实例中的原值进行加1操作,并返回最终相加后的结果

public final int incrementAndGet()

addAndGet(int delta) :以原子方式将输入的数值与实例中原本的值相加,并返回最后的结果

public final int addAndGet(int delta)

代码示例:

public class AtomicIntegerDemo {

    static AtomicInteger sum = new AtomicInteger(0);

    public static void main(String[] args) {

        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    // 原子自增  CAS
                    sum.incrementAndGet();
                    //TODO
                }
            });
            thread.start();
        }

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(sum.get());

    }
}
运行结果:
100000

我们看下它的JDK源码:

public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

/**
 * 通过do-while循环自选的方式来实现原子自增。
 * 
 * 首先获取字段的内存值,然后通过CAS操作更新字段值,
 * 更新失败后继续执行循环体语句获取新的内存值,
 * 更新成功后返回更新前的内存值,通过上层方法返回更新后的值。
 * 
 * @param var1 对象实例,即AtomicInteger的实例
 * @param var2 AtomicInteger对象中value字段的偏移量
 * @param var4 自增1
 * @return
 */
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;
}

从源码中可以看到,原子类自增操作是自旋 + CAS操作来实现。
这种CAS失败自选的操作存在什么问题?
长时间自选,会占用CPU资源。

数组类型原子类使用

AtomicIntegerArray为例总结常用的方法:

addAndGet(int i, int delta):以原子更新的方式将数组中索引为i的元素与输入值相加

public final int addAndGet(int i, int delta)

getAndIncrement(int i):以原子更新的方式将数组中索引为i的元素自增加1

public final int getAndIncrement(int i)

compareAndSet(int i, int expect, int update):将数组中索引为i的位置的元素进行更新

public final boolean compareAndSet(int i, int expect, int update)

代码示例:

public class AtomicIntegerArrayDemo {

    static int[] value = new int[]{ 1, 2, 3, 4, 5 };
    static AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(value);

    public static void main(String[] args) throws InterruptedException {
        //设置索引0的元素为100
        atomicIntegerArray.set(0, 100);
        System.out.println(atomicIntegerArray.get(0));
        //以原子更新的方式将数组中索引为1的元素与输入值相加
        atomicIntegerArray.getAndAdd(1,5);
        System.out.println(atomicIntegerArray);
    }
    
}
运行结果:
100
[100, 7, 3, 4, 5]

引用类型原子类使用

AtomicReference作用是对普通对象的封装,它可以保证你在修改对象引用时的线程安全性。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值