【线程】CAS

我们先知道一个常识,在多个线程同时操作变量进行++的操作的时候,如果不加锁,肯定会导致数据错误的情况
这种情况下,我们就可以通过加锁来实现并发不出错误,将要修改的方法加synchronized锁,这样同一时间只能有一个线程获得变量,这样也会导致问题

  • 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度掩饰,引起性能问题
  • 一个线程持有锁会导致其他所有需要此锁的线程进入阻塞状态(挂起)
  • 如果一个优先级高的线程等待一个优先级低的线程会导致优先级倒置,引起性能风险
    这个时候选择volatile是不错的机制,但是volatile不保证原子性,所以还是要回到锁机制上来

独占锁是一种悲观锁,synchronized就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁用到的机制就是CAS,Compar and Swap。

什么是CAS

compare and swap,中文翻译成比较并交换
CAS主要包括三个参数

  • 内存位置(V)
  • 原预期值(A)
  • 新值(B)

CAS工作流程

    1. 从地址V中读取值A
    1. 按照程序要求获得值B
    1. 用CAS将V的值从A改为B,如果这段时间V中的值还是A没有改变,就可以操作成功
      我们可以拿原子类的自增举例
public static void main(String[] args) {
        AtomicInteger a = new AtomicInteger();
        a.getAndIncrement();
    }

我们进入getAndIncrement()方法
在这里插入图片描述
可以看到这个方法就是调用了一个unsafe类的方法然后就返回了,那么这个unsafe类又是什么呢
我们点进getAndAddInt方法在这里插入图片描述
我们可以看到这里的本质就是一个自旋锁,不断的获得对应内存地址中的值,然后如果这个值在执行操作的时候还没有改变就对这个值加一

使用CAS的好处

因为CAS底层是native方法,调用C的特性来对内存进行直接操作,可以大大提高性能

使用CAS存在的问题

CAS虽然能很高效的解决原子操作,但是仍然存在问题

  • ABA问题
    因为CAS只是判断获取值和在操作时这个值之间的时间该没改变来进行操作,当在这个时间内如果有一个操作修改了这个内存变量的值,由A改为B再改为A,这时CAS会认为这个值从来没有变过,但是值其实已经发生了一次改变
  • 循环时间长时开销大
    因为底层是自旋锁,当操作迟迟无法完成的时候,会对CPU带来非常大的开销
  • 只能保证一个共享变量的原子操作
    当对多个共享变量进行原子操作时,循环CAS就无法保证操作的原子性

CAS中ABA问题的解决

CAS并非完美的,它会导致ABA问题,例如:当前内存的值一开始是A,被另外一个线程先改为B然后再改为A,那么当前线程访问的时候发现是A,则认为它没有被其他线程访问过。在某些场景下这样是存在错误风险的。比如在链表中。如何解决这个ABA问题呢,大多数情况下乐观锁的实现都会通过引入一个版本号标记这个对象,每次修改版本号都会变话,比如使用时间戳作为版本号,这样就可以很好的解决ABA问题。在JDK中提供了 AtomicStampedReference类来解决这个问题,这个类维护了一个int类型的标记stamp,每次更新数据的时候顺带更新一下stamp。

原子性 — Atomic包

在Java jdk中里面提供了很多Atomic类

AtomicXXX:CAS、Unsafe.compareAndSwapInt
AtomicLong、LongAdder
AtomicReference、AtomicReferenceFieldUpdater
AtomicStampReference:CAS的ABA问题
由于CAS原语的直接操作与计算机底层的联系很大,CAS原语有三个参数, 内存地址、 期望值、 新值。我们在Java中一般不去直接写CAS相关的代码,JDK为我们封装在AtomicXXX中,因此,我们直接使用就可以了。

我们在 java.util.concurrent.atomic目录中可以看到我们这些类,包下提供了 AtomicBoolean、 AtomicLong、 AtomicInteger三种原子更新基本类型和一个比较好玩的类 AtomicReference,这些类都有一个共同点,都支持CAS,以 AtomicInteger为重点讲解。

在这里插入图片描述

AtomicInteger

AtomicInteger是一个提供原子操作的Integer类,通过线程安全的方式操作加减,以下是 AtomicIntege基本包括的方法:

public final int getAndSet(int newValue)       //给AtomicInteger设置newValue并返回加oldValue 
public final boolean compareAndSet(int expect, int update)    //如果输入的值和期望值相等就set并返回true/false 
public final int getAndIncrement()     //对AtomicInteger原子的加1并返回当前自增前的value 
public final int getAndDecrement()   //对AtomicInteger原子的减1并返回自减之前的的value 
public final int getAndAdd(int delta)   //对AtomicInteger原子的加上delta值并返加之前的value 
public final int incrementAndGet()   //对AtomicInteger原子的加1并返回加1后的值 
public final int decrementAndGet()    //对AtomicInteger原子的减1并返回减1后的值 
public final int addAndGet(int delta)   //给AtomicInteger原子的加上指定的delta值并返回加后的值

示例

import lombok.extern.slf4j.Slf4j; 
 
import java.util.concurrent.CountDownLatch; 
import java.util.concurrent.ExecutorService; 
import java.util.concurrent.Executors; 
import java.util.concurrent.Semaphore; 
import java.util.concurrent.atomic.AtomicInteger; 
 
@Slf4j 
public class AtomicIntegerExample { 
 
    // 请求总数 
    public static int clientTotal = 5000; 
 
    // 同时并发执行的线程数 
    public static int threadTotal = 200; 
 
    public static AtomicInteger count = new AtomicInteger(0); 
 
    public static void main(String[] args) throws Exception { 
      //获取线程池 
        ExecutorService executorService = Executors.newCachedThreadPool(); 
        //定义信号量 
        final Semaphore semaphore = new Semaphore(threadTotal); 
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal); 
        for (int i = 0; i < clientTotal ; i++) { 
            executorService.execute(() -> { 
                try { 
                    semaphore.acquire(); 
                    add(); 
                    semaphore.release(); 
                } catch (Exception e) { 
                    log.error("exception", e); 
                } 
                countDownLatch.countDown(); 
            }); 
        } 
        countDownLatch.await(); 
        executorService.shutdown(); 
        log.info("count:{}", count.get()); 
    } 
 
    private static void add() { 
        count.incrementAndGet(); 
    } 
} 

这里我们使用请求总数为:5000,同时执行的并发线程数为:200,我们最终需要得到结果为:5000,这个执行结果才算正确。

查看返回结果:

13:43:26.473 [main] INFO com.mmall.concurrency.example.atomic.AtomicIntegerExample - count:5000 

最后结果是 5000表示是线程安全的。

我们来看看 AtomicInteger底层代码中到底为我们做了什么?首先我们来看 AtomicInteger.incrementAndGet() 方法

public class AtomicInteger extends Number implements java.io.Serializable{ 
/** 
     *  对AtomicInteger原子的加1并返回加1后的值 
     * @return 更新的值 
     */ 
    public final int incrementAndGet() { 
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1; 
    } 
} 

AtomicInteger 调用了java底层的unsafe.getAndAddInt()```方法,这里是实现CAS 的关键。

incrementAndGet()是将自增后的值返回,还有一个方法 getAndIncrement()是将自增前的值返回,分别对应 ++i和 i++操作。同样的 decrementAndGet()和 getAndDecrement()则对 --i和i–操作。

Unsafe类是在sun.misc包下,不属于Java标准。但是很多Java的基础类库,包括一些被广泛使用的高性能开发库都是基于
Unsafe类开发的,比如Netty、Cassandra、Hadoop、Kafka等。Unsafe类在提升Java运行效率,增强Java语言底层操作
能力方面起了很大的作用。Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力,同时也带来了指针的问题。
过度的使用Unsafe类会使得出错的几率变大,因此Java官方并不建议使用的,官方文档也几乎没有。通常我们最好也不
要使用Unsafe类,除非有明确的目的,并且也要对它有深入的了解才行。

再来看 Unsafe.getAndAddInt()方法

/* 
   * 其中getIntVolatile和compareAndSwapInt都是native方法 
   * getIntVolatile是获取当前的期望值 
   * compareAndSwapInt就是我们平时说的CAS(compare and swap),通过比较如果内存区的值没有改变,那么就用新值直接给该内存区赋值 
   */ 
    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; 
    } 
     
    public native int getIntVolatile(Object var1, long var2); 
    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5); 

我们可以看到 getAndAddInt(Objectvar1,longvar2,intvar4),传进来的第一个参数是当前的一个对象,也就是我们的:count.incrementAndGet(),在 getAndAddInt()中,var1就是count,var2就是当前的值,比如当前循环中count值为 2,var4为每次递增1

其次 getAndAddInt()方法中涉及到的两个方法调用都定义为native,即java底层实现的本地方法

  • getIntVolatile():获取保存当前对象count的主存地址的引用(注意不是对象的值,是引用)。
  • compareAndSwapInt():比较当前对象的值和底层该对象的值是否相等,如果相等,则将当前对象值加1,如果不相等,则重新去获取底层该对象的值,这个方法的实现就是CPU的CAS(compare and swap)操作。

我们知道 volatile具有一致性的特征,但是它不具备原子性,为什么 AtomicInteger却同时具备一致性和原子性,原来在 AtomicInteger源码中实现了这样一串代码:privatevolatileintvalue;,在 AtomicInteger内部实现就使用了 volatile关键字,这就是为什么执行CAS(对CAS有疑问的点击此处)操作的时候,从底层获取的数据就是最新的数据:

如果当前要保存的值和内存中最新的值不相等的话,说明在这个过程中被其他线程修改了,只能获取更新当前值为最新值,再那这个当前值再去和重新去内存获取的最新值比较,直到二者相等的时候,才完成+1的过程.

使用 AtomicInteger的好处在于,它不同于 sychronized关键字或 lock用锁的形式来实现原子性,加锁会影响性能,而是采用循环比较的形式来提高性能。

AtomicLong

AtomicLong 是作用是对长整形进行原子操作,依靠底层的cas来保障原子性的更新数据,在要添加或者减少的时候,会使用死循环不断地cas到特定的值,从而达到更新数据的目的。


import lombok.extern.slf4j.Slf4j; 
 
import java.util.concurrent.CountDownLatch; 
import java.util.concurrent.ExecutorService; 
import java.util.concurrent.Executors; 
import java.util.concurrent.Semaphore; 
import java.util.concurrent.atomic.AtomicLong; 
 
@Slf4j 
public class AtomicLongExample { 
 
    // 请求总数 
    public static int clientTotal = 5000; 
 
    // 同时并发执行的线程数 
    public static int threadTotal = 200; 
 
    public static AtomicLong count = new AtomicLong(0); 
 
    public static void main(String[] args) throws Exception { 
        ExecutorService executorService = Executors.newCachedThreadPool(); 
        final Semaphore semaphore = new Semaphore(threadTotal); 
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal); 
        for (int i = 0; i < clientTotal ; i++) { 
            executorService.execute(() -> { 
                try { 
                    semaphore.acquire(); 
                    add(); 
                    semaphore.release(); 
                } catch (Exception e) { 
                    log.error("exception", e); 
                } 
                countDownLatch.countDown(); 
            }); 
        } 
        countDownLatch.await(); 
        executorService.shutdown(); 
        log.info("count:{}", count.get()); 
    } 
 
    private static void add() { 
        count.incrementAndGet(); 
        // count.getAndIncrement(); 
    } 
} 

执行结果:

14:59:38.978 [main] INFO com.mmall.concurrency.example.atomic.AtomicLongExample - count:5000 

最后结果是 5000表示是线程安全的。

AtomicBoolean

AtomicBoolean位于java.util.concurrent.atomic包下,是java提供给的可以保证数据的原子性操作的一个类

import lombok.extern.slf4j.Slf4j; 
 
import java.util.concurrent.CountDownLatch; 
import java.util.concurrent.ExecutorService; 
import java.util.concurrent.Executors; 
import java.util.concurrent.Semaphore; 
import java.util.concurrent.atomic.AtomicBoolean; 
 
@Slf4j 
public class AtomicBooleanExample { 
 
    private static AtomicBoolean isHappened = new AtomicBoolean(false); 
 
    // 请求总数 
    public static int clientTotal = 5000; 
 
    // 同时并发执行的线程数 
    public static int threadTotal = 200; 
 
    public static void main(String[] args) throws Exception { 
        ExecutorService executorService = Executors.newCachedThreadPool(); 
        final Semaphore semaphore = new Semaphore(threadTotal); 
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal); 
        for (int i = 0; i < clientTotal ; i++) { 
            executorService.execute(() -> { 
                try { 
                    semaphore.acquire(); 
                    test(); 
                    semaphore.release(); 
                } catch (Exception e) { 
                    log.error("exception", e); 
                } 
                countDownLatch.countDown(); 
            }); 
        } 
        countDownLatch.await(); 
        executorService.shutdown(); 
        log.info("isHappened:{}", isHappened.get()); 
    } 
 
    private static void test() { 
        if (isHappened.compareAndSet(false, true)) { 
            log.info("execute"); 
        } 
    } 
} 

返回结果

15:04:54.954 [pool-1-thread-2] INFO com.mmall.concurrency.example.atomic.AtomicBooleanExample - execute 
15:04:54.971 [main] INFO com.mmall.concurrency.example.atomic.AtomicBooleanExample - isHappened:true 

这里我们发现 log.info(“execute”);,在代码中只执行了一次,并且 isHappened:true的值为true,这是为啥呢?

这里是因为当程序第一次 compareAndSet()的时候,使得 isHappend变为了true,因为原子性的关系,没有其他线程进行干扰,通过使用AtomicBoolean,我们使某段代码只执行一次。

AtomicReference

AtomicReference和 AtomicInteger非常类似,不同之处就在于 AtomicInteger是对整数的封装,底层采用的是 compareAndSwapInt实现CAS,比较的是数值是否相等,而 AtomicReference则对应普通的对象引用,底层使用的是 compareAndSwapObject实现CAS,比较的是两个对象的地址是否相等。也就是它可以保证你在修改对象引用时的线程安全性。

多个线程之间的操作无论采用何种执行时序或交替方式,都要保证不变性条件不被破坏,要保持状态的一致性,就需要在单个原子操作中更新相关的状态变量。

import lombok.extern.slf4j.Slf4j; 
 
import java.util.concurrent.atomic.AtomicReference; 
 
@Slf4j 
public class AtomicReferenceExample { 
 
    private static AtomicReference<Integer> count = new AtomicReference<>(0); 
 
    public static void main(String[] args) { 
        count.compareAndSet(0, 2);  
        count.compareAndSet(0, 1); 
        count.compareAndSet(1, 3);  
        count.compareAndSet(2, 4);  
        count.compareAndSet(3, 5);  
        log.info("count:{}", count.get()); 
    } 
} 

大家觉得我们输出的结果会是多少?

返回结果:

15:26:59.680 [main] INFO com.mmall.concurrency.example.atomic.AtomicReferenceExample - count:4 


为什么是4呢?首先我们 要说的是 publicfinalbooleancompareAndSet(V expect,V update)这个方法,这个方法主要的作用是通过比对两个对象,然后更新为新的对象,这里的比对两个对象,比对的方式不是 equals而是 ==,意味着比对的是内存的中地址。

1.首先我们创建 count的初始化为0
2. 在main方法中 count.compareAndSet(0,2);,判断count为0时赋值为2
3. 在 count.compareAndSet(0,1);和 count.compareAndSet(1,3);判断count是否为1或者0,因为上一步我们已经赋值为2了,所以判断不成立
4. 在 count.compareAndSet(2,4);判断count是否为2,等式成立
5. 最好输出结果为4

count.compareAndSet(0, 2); //count=0?赋值 2,判断成立,此时count=0,更新后为2 
count.compareAndSet(0, 1); //count=0?赋值 1,判断不成立,此时count=2 
count.compareAndSet(1, 3); //count=1?赋值 3,判断不成立,此时count=2 
count.compareAndSet(2, 4); //count=2?赋值 4,判断成立,此时count=2,更新后count=4 
count.compareAndSet(3, 5); //count=3?赋值 5,判断不成立,此时count=4 

所以我们输出结果为:4

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值