什么是CAS?

Posted Dream_it_possible!

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了什么是CAS?相关的知识,希望对你有一定的参考价值。

目录

为什么会有CAS?

AtomicInteger是什么?

CAS用法演示

ABA问题

ABA解决方案

CAS应用实战

基于CAS手写一把互斥锁


         CAS是CompareAndSet的缩写,它是一组操作的整合,CAS将当前值与期望的值做比较,如果当前值与期望值相等,那么就将新值给更新进去作为当前值,否则不会更新新值给当前值。CAS是Java在多线程并发场景下提供的一种非阻塞的机制,具有原子性,是一种避免加锁的也能实现线程安全的,它的实现是在JVM层面上的。

为什么会有CAS?

        举一个生活中最常见的例子,小刚要给小红转账100块钱,小刚的账户有1000块钱,那么小刚转账给小红的这个动作可以分解为2个步骤:  1) 小刚的账户余额减100元。2) 将900元赋值给小刚的账户余额。这2个动作要么全部执行成功,要么全部执行失败,因此小刚转账的动作需要具备原子性。

        我们可以把小刚账户的钱当做一个整型的变量放入到AtomicInteger里,AtomicInteger提供了很多具有原子性的方法,那么小刚转账的操作使用getAndAdd(-100)就能够保证原子性,底层实现是CAS,CAS的出现就能解决类似的问题。

        刚提到有用AtomicInteger保证整型变量的原子性, 那这个到底是个什么玩意呢,接着看AtomicInteger。

AtomicInteger是什么?

        AtomicInteger在atomic包下的一个保证整型变量执行加减等操作原子性的类,atomic在java.util.concurrent包下,java.util.concurrent简称JUC,该包下还有AtomicBoolean,AtomicLong,AtomicReference用法类似,下面只看AtomicInteger的用法和原理。

AtomicInteger类里几个核心属性:

unsafe:  提供CAS操作,并且提供类字段的在内存中的地址偏移量获取方法。

valueOffset:  共享属性在类AtomicInteger对象的内存所在的地址偏移量。

value: 由volatile关键字修饰,标记AtomicInteger当前的值,保证共享变量在多线程中的可见性。

public class AtomicInteger extends Number implements java.io.Serializable 
    private static final long serialVersionUID = 6214790243416807050L;

    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static 
        try 
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
         catch (Exception ex)  throw new Error(ex); 
    

    private volatile int value;

    /**
     * Creates a new AtomicInteger with the given initial value.
     *
     * @param initialValue the initial value
     */
    public AtomicInteger(int initialValue) 
        value = initialValue;
    
...

 这些属性是AtomicInteger实现原子操作的核心和关键,其中unsafe类是sun公司提供的在JVM层面上能对共享的变量进行原子操作的类,包含了很多native修饰的方法以及CAS操作的方法:

    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);

AtomicInteger的compareAndSet()方法的底层实现就是compareAndSwapInt

   public final boolean compareAndSet(int expect, int update) 
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    

unsafe的实现都是在JVM层面上的,如果想了解更多,那么需要看汇编层面上的代码。

CAS用法演示

        i+=1是一个非原子的操作,但如果你用addAndGet(1)方法执行,就能保证i+1的原子性。

package com.example.jucdemo;

import java.util.concurrent.atomic.AtomicInteger;

public class BankDemo 

    private static AtomicInteger wallet = new AtomicInteger(1000);

    private static int money = 1000;

    public static void main(String[] args) 

        for (int i = 0; i < 100; i++) 
            new Thread(new safeSubTask()).start();
        
        try 
            Thread.sleep(1000);
         catch (InterruptedException e) 
            e.printStackTrace();
        
        System.out.println("余额:" + wallet.get());
        for (int j = 0; j < 100; j++) 
            new Thread(new subTask()).start();
        

        try 
            Thread.sleep(1000);
         catch (InterruptedException e) 
            e.printStackTrace();
        
        System.out.println("余额:" + money);

    

    static class safeSubTask implements Runnable 


        @Override
        public void run() 
            wallet.addAndGet(-1);
            System.out.println(Thread.currentThread().getName() + "花了1块钱!");
        
    

    static class subTask implements Runnable 

        @Override
        public void run() 
            money -= 1;
            System.out.println(Thread.currentThread().getName() + "花了1块钱!");
        
    


打印结果如下: 

 由打印可知,AtomicInteger能提供对变量操作的原子性,而没用AtomicInteger出现了异常的结果!

ABA问题

        虽然说CAS能保证线程安全,但是存在着有线程将变量从A更新到B,然后从B又更新到A的问题,貌似该线程并没有用过共享变量,因此会存在潜在的风险,例如场景:

        给定一个链表,如果链表中的某个节点没有修改,那么进行CAS操作。 假如此时的线程1 拿到了该链表里的头节点的值A, 线程1将该值更新为了B,然后线程2再次拿到头节点,将头节点的值从B更新为A,那么线程3来的时候就懵了,他会觉得这个头节点没有被动过,那么应该需要进行CAS操作。

        这样问题就来了,其实头节点已经被2个线程修改过,但是线程3仍然认为头节点没有被修改,因此CAS有潜在的ABA问题,这种场景很少见,问题也比较刁钻,有没有解决方法呢?

ABA解决方案

        解决ABA的问题,主要有2种方案,一种是用AtomicStampedReferemce, 另外一个是AtomicMarkableReference。AtomicStampedReference内部维护了一个整型值stamp,类似于版本号的作用,修改了那么表示已经变更, AtomicMarkableReference内部则是维护了一个布尔值mark, 如果mark被更新了那么表示值已经被更改,那么就不会进行CAS操作。

CAS应用实战

基于CAS手写一把互斥锁

        Unsafe不能采用Unsafe.getUnsafe()的方式获取,否则的话会报securityException。我们可以从Unsafe里定义的unsafe属性获取到Unsafe对象。

package cas;

import sun.misc.Unsafe;

import java.lang.reflect.Field;

public class UnsafeFactory 


    public static Unsafe getUnsafe() 

        try 
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            return (Unsafe) field.get(null);
         catch (NoSuchFieldException | IllegalAccessException e) 
            e.printStackTrace();
        
        return null;
    


        实现CAS操作,需要定义共享变量state在内存中的地址偏移量offset。

    private static Unsafe unsafe = UnsafeFactory.getUnsafe();

    private static Long offset;
    private volatile int state;



    static 
        try 
            offset = unsafe.objectFieldOffset
                    (CasLock.class.getDeclaredField("state"));
         catch (Exception ex) 
            throw new Error(ex);
        
    

        有了offset后,那么就能通过unsafe获取到state变量在内存中的地址,也就是说能进行CAS操作,  举个例子,Unsafe类里的compareAndSwapInt方法,比较并设置新的整型值给state:

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

下面是参数解释:

  • var1:  表示当前类的对象。
  • var2:  类对象所在内存的地址偏移量,指向state属性。
  • var3: 当前值,必须为整型。
  • var4: 新值,必须为整型。

加锁:

  /**
     * 加锁, 互斥锁,在JVM层面上实现
     */
    public boolean lock() 
        return unsafe.compareAndSwapInt(this, offset, 0, 1);
    

解锁:

 /**
     * 释放锁
     */
    public boolean release() 
        return unsafe.compareAndSwapInt(this, offset, 1, 0);
    

CASLock完整代码:

package cas;

import sun.misc.Unsafe;

public class CasLock 


    private static Unsafe unsafe = UnsafeFactory.getUnsafe();

    private static Long offset;
    private volatile int state;



    static 
        try 
            offset = unsafe.objectFieldOffset
                    (CasLock.class.getDeclaredField("state"));
         catch (Exception ex) 
            throw new Error(ex);
        
    

    public CasLock() 
        this.state = 0;
    

    public CasLock(int state) 
        this.state = state;
    

    public boolean cas() 
        return unsafe.compareAndSwapInt(this, offset, 0, 1);
    

    /**
     * 加锁, 互斥锁,在JVM层面上实现
     */
    public boolean lock() 
        return unsafe.compareAndSwapInt(this, offset, 0, 1);
    


    /**
     * 释放锁
     */
    public boolean release() 
        return unsafe.compareAndSwapInt(this, offset, 1, 0);
    



用刚实现的CASLock替换掉ReentrantLock实现同步:

import cas.CasLock;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.ReentrantLock;


/**
 * 卖面包
 */
public class ReentrantLockDemo 

    private static ReentrantLock reentrantLock = new ReentrantLock();

    /**
     * 使用caslock
     */
    private static CasLock casLock = new CasLock();

    private static int num = 5;


    private static volatile int money = 100;

    static int threadNums = 10;


    public static void main(String[] args) 

        for (int i = 0; i < threadNums; i++) 
            Thread t = new Thread(new CurrentTask());
            t.start();
//            try 
//                t.join();
//             catch (InterruptedException e) 
//                e.printStackTrace();
//            
        

        try 
            Thread.sleep(3000);
         catch (InterruptedException e) 
            e.printStackTrace();
        
        System.out.println("面包售卖结束!");

//        Thread-0获取到一个面包,剩余数量:4,剩余钱数:90
//        Thread-1获取到一个面包,剩余数量:3,剩余钱数:80
//        Thread-0充值:20 RMB,剩余钱数:90
//        Thread-2获取到一个面包,剩余数量:2,剩余钱数:70
//        Thread-3获取到一个面包,剩余数量:1,剩余钱数:100
//        Thread-1充值:20 RMB,剩余钱数:110
//        Thread-4获取到一个面包,剩余数量:0,剩余钱数:130
//        Thread-4充值:20 RMB,剩余钱数:150
//        Thread-3充值:20 RMB,剩余钱数:130
//        Thread-2充值:20 RMB,剩余钱数:120
//        对不起,面包卖完了!
//                对不起,面包卖完了!
//                对不起,面包卖完了!
//                最后剩余钱数:150
//        对不起,面包卖完了!
//                对不起,面包卖完了!
//                Process finished with exit code 0

    


    static class SubTask implements Runnable 

        @Override
        public void run() 
            if (num <= 0) 
                System.out.println("对不起,面包卖完了!");
                return;
            
            money -= 10;
            System.out.println(Thread.currentThread().getName() + "获取到一个面包,剩余数量:" + --num + ",剩余钱数:" + money);
            // 充值20元
            money += 20;
            System.out.println(Thread.currentThread().getName() + "充值:20 RMB,剩余钱数:" + money);
        
    

    static class CurrentTask implements Runnable 

        @Override
        public void run() 
            if (casLock.lock()) 
//            reentrantLock.lock();
                if (num <= 0) 
                    System.out.println("对不起,面包卖完了!");
//                    reentrantLock.unlock();
                    return;
                
                money -= 10;
                System.out.println(Thread.currentThread().getName() + "获取到一个面包,剩余数量:" + --num + ",剩余钱数:" + money);
                // 充值20元
                money += 20;
                System.out.println(Thread.currentThread().getName() + "充值:20 RMB,剩余钱数:" + money);
//            reentrantLock.unlock();
                casLock.release();
            
        
    

打印结果:

Thread-0获取到一个面包,剩余数量:4,剩余钱数:90
Thread-0充值:20 RMB,剩余钱数:110
Thread-3获取到一个面包,剩余数量:3,剩余钱数:100
Thread-3充值:20 RMB,剩余钱数:120
Thread-5获取到一个面包,剩余数量:2,剩余钱数:110
Thread-5充值:20 RMB,剩余钱数:130
Thread-9获取到一个面包,剩余数量:1,剩余钱数:120
Thread-9充值:20 RMB,剩余钱数:140
面包售卖结束!

从打印结果发现面包并没有卖完, 因为是互斥的有的线程会出现争夺失败的情况,继续在此基础上完善,添加一个自旋和volatile开关。

import cas.CasLock;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.ReentrantLock;


/**
 * 卖面包
 */
public class ReentrantLockDemo 

    private static ReentrantLock reentrantLock = new ReentrantLock();

    /**
     * 使用caslock
     */
    private static CasLock casLock = new CasLock();

    private static int num = 5;


    private static volatile int money = 100;

    static int threadNums = 10;

    private static volatile boolean sellout = false;


    public static void main(String[] args) 

        for (int i = 0; i < threadNums; i++) 
            Thread t = new Thread(new CurrentTask());
            t.start();
//            try 
//                t.join();
//             catch (InterruptedException e) 
//                e.printStackTrace();
//            
        

        try 
            Thread.sleep(3000);
         catch (InterruptedException e) 
            e.printStackTrace();
        
        System.out.println("面包售卖结束!");

//        Thread-0获取到一个面包,剩余数量:4,剩余钱数:90
//        Thread-1获取到一个面包,剩余数量:3,剩余钱数:80
//        Thread-0充值:20 RMB,剩余钱数:90
//        Thread-2获取到一个面包,剩余数量:2,剩余钱数:70
//        Thread-3获取到一个面包,剩余数量:1,剩余钱数:100
//        Thread-1充值:20 RMB,剩余钱数:110
//        Thread-4获取到一个面包,剩余数量:0,剩余钱数:130
//        Thread-4充值:20 RMB,剩余钱数:150
//        Thread-3充值:20 RMB,剩余钱数:130
//        Thread-2充值:20 RMB,剩余钱数:120
//        对不起,面包卖完了!
//                对不起,面包卖完了!
//                对不起,面包卖完了!
//                最后剩余钱数:150
//        对不起,面包卖完了!
//                对不起,面包卖完了!
//                Process finished with exit code 0

    


    static class SubTask implements Runnable 

        @Override
        public void run() 
            if (num <= 0) 
                System.out.println("对不起,面包卖完了!");
                return;
            
            money -= 10;
            System.out.println(Thread.currentThread().getName() + "获取到一个面包,剩余数量:" + --num + ",剩余钱数:" + money);
            // 充值20元
            money += 20;
            System.out.println(Thread.currentThread().getName() + "充值:20 RMB,剩余钱数:" + money);
        
    

    static class CurrentTask implements Runnable 

        @Override
        public void run() 
            while (!sellout) 
                if (casLock.lock()) 
//            reentrantLock.lock();
                    if (num <= 0) 
                        System.out.println("对不起,面包卖完了!");
//                    reentrantLock.unlock();
                        sellout = true;
                        return;
                    
                    money -= 10;
                    System.out.println(Thread.currentThread().getName() + "获取到一个面包,剩余数量:" + --num + ",剩余钱数:" + money);
                    // 充值20元
                    money += 20;
                    System.out.println(Thread.currentThread().getName() + "充值:20 RMB,剩余钱数:" + money);
//            reentrantLock.unlock();
                    casLock.release();
                
            
        
    

打印结果: 

Thread-0获取到一个面包,剩余数量:4,剩余钱数:90
Thread-0充值:20 RMB,剩余钱数:110
Thread-2获取到一个面包,剩余数量:3,剩余钱数:100
Thread-2充值:20 RMB,剩余钱数:120
Thread-1获取到一个面包,剩余数量:2,剩余钱数:110
Thread-1充值:20 RMB,剩余钱数:130
Thread-0获取到一个面包,剩余数量:1,剩余钱数:120
Thread-0充值:20 RMB,剩余钱数:140
Thread-2获取到一个面包,剩余数量:0,剩余钱数:130
Thread-2充值:20 RMB,剩余钱数:150
对不起,面包卖完了!
面包售卖结束!

Process finished with exit code 0

自旋能继续尝试获取CASLock,通过添加 sellout变量实现结束售卖的行为。

以上是关于什么是CAS?的主要内容,如果未能解决你的问题,请参考以下文章

Java -- 每日一问:AtomicInteger底层实现原理是什么?如何在自己的产品代码中应用CAS操作?

[多线程进阶]CAS与Synchronized基本原理

ceph存储领域的CAS是什么?什么是CAS|Open CAS

CAS浅析

CAS 原理 应用

CAS 原理 应用