继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

juc-06-CAS与Atomic原子类

黑桃SEVEN_PIG
关注TA
已关注
手记 46
粉丝 11
获赞 8

这篇文章,说说 CAS,还有Java java.util.concurrent.atomic 包下的 Atomic 原子类。

1、什么是CAS

CAS:Compare and Swap,即比较再交换。cpu的特殊指令,指令级别保证这是一个原子操作。
CAS 需要有3个操作数:内存值V,旧的预期值A,即将要更新的目标值B。 CAS指令执行时,当且仅当内存值V的值与预期值A相等时,将内存值V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。

通常在自旋(死循环)中不断地进行 CAS 操作。

2、模仿 CAS

/**
 * Description: 模仿 CAS
 *
 * @author Xander
 * datetime: 2020-11-13 7:02
 */
public class MockCas {

    /**
     * 内存值
     */
    private volatile int value;

    public MockCas(int value) {
        this.value = value;
    }

    /**
     * 模仿 CAS
     *
     * @param expectedValue 预期值
     * @param newValue      要更新的新值
     * @return
     */
    public synchronized int compareAndSwap(int expectedValue, int newValue) {
        int oldValue = value;
        if (oldValue == expectedValue) {
            value = newValue;
            System.out.println(Thread.currentThread().getName() + " cas 操作成功");
        } else {
            System.out.println(Thread.currentThread().getName() + " cas 操作失败");
        }
        return oldValue;
    }

    public static void main(String[] args) throws InterruptedException {
        // 内存值是 100
        int value = 100;
        int expectedValue = 100;
        MockCas cas = new MockCas(value);
        Thread thread0 = new Thread(() -> {
            int oldValue = cas.compareAndSwap(expectedValue, 101);
            System.out.println(Thread.currentThread().getName() + " cas 内存值:" + oldValue);
        });
        Thread thread1 = new Thread(() -> {
            int oldValue = cas.compareAndSwap(expectedValue, 101);
            System.out.println(Thread.currentThread().getName() + " cas 内存值:" + oldValue);
        });
        thread0.start();
        thread1.start();
        thread0.join();
        thread1.join();
    }
}

运行结果:

Thread-0 cas 操作成功
Thread-0 cas 内存值:100
Thread-1 cas 操作失败
Thread-1 cas 内存值:101

3、Java中原子类

原子类的作用和锁类似,是为了保证并发情况下线程安全。
不过原子类相比于锁,有一定的优势
1)粒度更细:原子变量可以把竞争范围缩小到变量级别,这是我们可以获得的最细粒度的情况了,通常锁的粒度都要大于原子变量的粒度
2)效率更高:通常,使用原子类的效率会比使用锁的效率更高。

Jdk java.util.concurrent.atomic 包中提供了 6 类原子类

  • Atomic* 基本类型原子类:AtomicBoolean,AtomicInteger,AtomicLong
  • Atomic*Array 数组类型原子类:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
  • Atomic*Reference 引用类型原子类:AtomicReference,AtomicMarkableReference,AtomicStampedReference
  • Atomic*FieldUpdater 升级原子类: AtomicReferenceFieldUpdater,AtomicIntegerFieldUpdater,AtomicLongFieldUpdater
  • Adder累加器LongAdder、DoubleAdder
  • Accumulator累加器LongAccumulator、DoubleAccumulator

4、Java 是如何利用 CAS 实现原子操作的

原子操作,是不可分割,不可中断的,即便是多线程的情况下也可以保证。

Unsafe类

Unsafe 是CAS的核心类。Java无法直接访问低层操作系统,而是通过本地方法(native)来进行访问。不过尽管如此,JVM还是开了一个后门,JDK中有一个类 Unsafe ,它提供了硬件级别的原子操作。

valueOffset表示的是变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的原值的,就这样我们就能通过 Unsafe 来实现CAS了。

AtomicInteger 为例,分析在Java中是如何利用 CAS 实现原子操作的?
AtomicInteger 加载 Unsafe 工具,用来直接操作内存数据,用 Unsafe 来实现低层 CAS 操作, 用 volatile 修饰 value 字段,保证可见性。

AtomicInteger getAndAdd()方法 源码

// AtomicInteger 部分源码
public class AtomicInteger extends Number implements java.io.Serializable {
...
// setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            // value 的内存偏移地址
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    // value值
    private volatile int value;

    /**
     * Atomically adds the given value to the current value.
     *
     * @param delta the value to add
     * @return the previous value
     */
    public final int getAndAdd(int delta) {
        // unsafe 中 do-while 循环,自旋,直至 cas 操作成功
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }
...
}

// Unsafe部分源码 getAndAddInt
public final class Unsafe {

...
    public final int getAndAddInt(Object var1, long var2, int var4) {
        // do-while 循环,自旋,不断地调用 compareAndSwapInt(cas操作) 直到成功
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

    // 低层的 cas 操作
    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

...
}

5、基础类型原子类,演示AtomicInteger的基本用法

对比非原子类的线程安全问题,使用了原子类之后,不需要加锁,也可以保证线程安全。

AtomicInteger 常用API:

  1. public final int get():获取当前的值
  2. public final int getAndSet(int newValue):获取当前的值,并设置新的值
  3. public final int getAndIncrement():获取当前的值,并自增1
  4. public final int getAndDecrement():获取当前值,并自减1
  5. public final int getAndAdd(int delta):获取当前的值,并加上预期的值
  6. public final boolean compareAndSet(int expect, int update):如果输入的等于预期值,则以原子方式将该值设置为输入值,设置成功返回true,否则 expect 不等于 内存值,返回false

/**
* Description: 演示AtomicInteger的基本用法,对比非原子类的线程安全问题,使用了原子类之后,不需要加锁,也可以保证线程安全。
* @author Xander
* datetime: 2020-11-13 12:53
*/
public class AtomicIntegerDemo implements Runnable {

    /**
     * 普通变量,对变量的操作非线程安全
     */
    private static volatile int var = 0;

    /**
     * 原子类变量,线程安全操作
     */
    private static final AtomicInteger atomicInteger = new AtomicInteger();

    /**
     * 对普通变量进行+1
     */
    public void incrementBasic() {
        var++;
    }

    /**
     * 对原子变量进行+1
     */
    public void incrementAtomic() {
        atomicInteger.getAndIncrement();
    }

    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            incrementAtomic();
            incrementBasic();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        AtomicIntegerDemo r = new AtomicIntegerDemo();
        Thread t1 = new Thread(r);
        Thread t2 = new Thread(r);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("原子类的结果:" + atomicInteger.get());
        System.out.println("普通变量的结果:" + var);
    }
}

运行结果:

原子类的结果:20000
普通变量的结果:19264

2个线程,分别进行10000次 +1 操作,可以看出原子变量的结果是符合预期的,而普通变量的结果则是不正确的。

6、数组类型原子类,演示 AtomicIntegerArray 的基本用法

  • AtomicIntegerArray(int length):根据给定的 length 初始化数组,新建长度为 length 的成员变量arrayarray = new int[length];
  • AtomicIntegerArray(int[] array):根据给定的数组 clone() 初始化成员变量的 arraythis.array = array.clone();
  • int addAndGet(int i, int delta):对数组中下标为 i 的元素添加 delta 并返回更新后的值
  • boolean compareAndSet(int i, int expect, int update):对数组中下标为 i 的元素传入 expect 值和 update 值,进行 cas 操作,如果 expect 值等于内存值,则修改成功,返回true,否则返回false
  • int decrementAndGet(int i):对数组中下标为 i 的元素 -1,然后返回修改后的值
  • int get(int i):返回数组中下标为 i 的元素的值
  • int getAndAdd(int i, int delta):对数组中下标为 i 的元素添加 delta 并返回更新前的值
  • int getAndDecrement(int i):对数组中下标为 i 的元素 -1,然后返回修改前的值
  • int getAndIncrement(int i):对数组中下标为 i 的元素 +1,然后返回修改前的值
  • int getAndSet(int i, int newValue):对数组中下标为 i 的元素自旋设值 newValue,然后返回修改前的值
  • int incrementAndGet(int i):对数组中下标为 i 的元素 +1,然后返回修改后的值
  • int length():返回数组的长度

两个线程并发对长度为 5 的普通数组变量和原子类 AtomicIntegerArray 的每个元素分别进行10000次+1,原子变量的结果是符合预期的,而普通数组变量的结果则是不正确的。

/**
 * 演示原子数组的使用方法
 */
public class AtomicArrayDemo implements Runnable {

    static int length = 5;
    /**
     * 普通数组变量,对变量的操作非线程安全
     */
    private static volatile int[] array = new int[length];

    /**
     * 原子类变量,线程安全操作
     */
    private static final AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(length);

    /**
     * 对普通变量进行+1
     */
    public void incrementBasic(int index) {
        array[index]++;
    }

    /**
     * 对原子变量进行+1
     */
    public void incrementAtomic(int index) {
        atomicIntegerArray.getAndIncrement(index);
    }

    @Override
    public void run() {
        for (int index = 0; index < length; index++) {
            for (int count = 0; count < 10000; count++) {
                incrementAtomic(index);
                incrementBasic(index);
            }
        }
    }


    public static void main(String[] args) throws InterruptedException {
        AtomicArrayDemo r = new AtomicArrayDemo();
        Thread t1 = new Thread(r);
        Thread t2 = new Thread(r);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("原子类的结果:" + atomicIntegerArray);
        System.out.println("普通变量的结果:");
        for (int item : array) {
            System.out.print(item + " ");
        }
    }
}

运行结果:

原子类的结果:[20000, 20000, 20000, 20000, 20000]
普通变量的结果:
19345 18226 17153 17765 19513 

7、引用类型原子类,演示 AtomicReference 的基本用法

  • V get():获取 AtomicReference 的 value ,是一个引用类型。
  • set(V newValue):设置 AtomicReference 的 新值
  • V getAndSet(V newValue):设置 AtomicReference 的 新值,并返回旧的值
/**
 * 演示引用类型的原子操作类
 */
public class AtomicReferenceDemo {
	
	static AtomicReference<UserInfo> userRef = new AtomicReference<UserInfo>();
	
    public static void main(String[] args) {
        UserInfo user = new UserInfo("Mark", 15);//要修改的实体的实例
        userRef.set(user);
        
        UserInfo updateUser = new UserInfo("Bill", 17);//要变化的新实例
        userRef.compareAndSet(user, updateUser);
        System.out.println(userRef.get().getName());
        System.out.println(userRef.get().getAge());
        System.out.println(user.getName());
        System.out.println(user.getAge());        
    }
    
    //定义一个实体类
    static class UserInfo {
        private String name;
        private int age;
        public UserInfo(String name, int age) {
            this.name = name;
            this.age = age;
        }
        public String getName() {
            return name;
        }
        public int getAge() {
            return age;
        }
    }

}

运行结果:

Bill
17
Mark
15

8、带版本戳引用类型原子类,演示 AtomicStampedReference 的基本用法


/**
 * 演示带版本戳的原子操作类
 */
public class AtomicStampedReferenceDemo {

    static AtomicStampedReference<String> asr =
            new AtomicStampedReference<>("Xander", 0);


    public static void main(String[] args) throws InterruptedException {
        final int oldStamp = asr.getStamp();//拿初始的版本号
        final String oldReferenc = asr.getReference();//拿初始的引用

        System.out.println(oldReferenc + "===========" + oldStamp);

        Thread rightStampThread = new Thread(new Runnable() {

            @Override
            public void run() {
                String reference = asr.getReference();
                System.out.println(Thread.currentThread().getName()
                        + "  修改前变量值:" + reference + "  修改前版本戳:" + asr.getStamp() + "  修改成功:"
                        + asr.compareAndSet(oldReferenc, oldReferenc + "Java", oldStamp, oldStamp + 1));
            }

        });

        Thread errorStampThread = new Thread(new Runnable() {

            @Override
            public void run() {
                String reference = asr.getReference();
                System.out.println(Thread.currentThread().getName()
                        + "  修改前变量值:" + reference + "  修改前版本戳:" + asr.getStamp() + "  修改成功:"
                        + asr.compareAndSet(reference, reference + "C", oldStamp, oldStamp + 1));

            }

        });

        rightStampThread.start();
        rightStampThread.join();
        errorStampThread.start();
        errorStampThread.join();
        System.out.println(asr.getReference() + "===========" + asr.getStamp());

    }
}

运行结果:

Xander===========0
Thread-0  修改前变量值:Xander  修改前版本戳:0  修改成功:true
Thread-1  修改前变量值:XanderJava  修改前版本戳:1  修改成功:false
XanderJava===========1

9、CAS的问题

ABA 问题

CAS 的理念是,在我修改值期间,没有其他人进行修改,我最后再修改值。

什么是 ABA 问题?
①线程1和线程2,同时需要对内存值V进行操作,同时读出当前内存值V=A
②线程1 准备修改为 A1 ,于是它准备执行 compareAndSwap(int expectedValue, int newValue),其中 expectedValue=A,newValue=A1;
③在线程1准备进行修改之前,线程2也对内存值V 进行修改,并且进行了两次修改,先从 A 修改为 B (compareAndSwap(A,B)),再由 B 修改为 A (compareAndSwap(B,A));
④线程1最后 compareAndSwap(A, A1),这时候,线程1也能够执行成功,但是会存在很多潜在的问题,因为在线程1修改期间,内存值V已经被多次修改。

怎么避免 ABA 问题?可以通过版本号解决,例如原子类AtomicStampedReference

开销问题

通常原子操作是在自旋(死循环)中不断地进行 CAS 操作直至成功,例如 Unsafe 类中的 getAndAddInt()
如果我们设计类似 Unsafe 类中的自旋算法时,需要注意如果 CAS 操作长期不成功,cpu不断的循环,会存在开销问题。

// Unsafe部分源码 getAndAddInt
public final class Unsafe {

...
    public final int getAndAddInt(Object var1, long var2, int var4) {
        // do-while 循环,自旋,不断地调用 compareAndSwapInt(cas操作) 直到成功
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

    // 低层的 cas 操作
    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

...
}

这篇文章给大家浅谈了什么是 CAS,给大家介绍了 JDK 中常用类型的 Atomic* 原子类的使用,我这里也是简单的介绍,相信大家读完这篇文章,理解 CAS 算法后,再去读其他原子类的源码,也是很简单的,最后说明了 CAS算法和自旋,可能会存在的 ABA问题和开销问题。

打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP