> 文章列表 > Java中CAS详解

Java中CAS详解

Java中CAS详解

1、概述

说到CAS就会想到Java中的原子类,也即是java.util.concurrent.atomic包下的类。

咱们先看看在多线程环境下对比使用原子类和不使用原子类怎么保证i++线程安全,以及性能结果。

实例代码:

500个线程,每个线程执行100万次i++

package com.lc.test03;import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicLong;/* @author liuchao* @date 2023/4/15*/
public class CasTest {volatile Long value = 0L;AtomicLong atomicLong = new AtomicLong(0L);public Long getValue() {return value;}public synchronized void addValueBySynchronized() {value++;}public synchronized void addValueByAtomic() {atomicLong.incrementAndGet();}public static void main(String[] args) throws Exception {/* 每个线程请求100万次*/int reqNum = 1000000;/* 500个线程*/int threadNum = 500;CasTest casTest1 = new CasTest();CountDownLatch countDownLatch1 = new CountDownLatch(threadNum);Long start = System.currentTimeMillis();for (int i = 0; i < threadNum; i++) {new Thread(() -> {try {for (int j = 0; j < reqNum; j++) {casTest1.addValueBySynchronized();}} finally {countDownLatch1.countDown();}}).start();}countDownLatch1.await();Long end = System.currentTimeMillis();System.out.println("Synchronized用时:" + (end - start) + "毫秒,最终值:" + casTest1.getValue());CasTest casTest2 = new CasTest();CountDownLatch countDownLatch2 = new CountDownLatch(threadNum);start = System.currentTimeMillis();for (int i = 0; i < threadNum; i++) {new Thread(() -> {try {for (int j = 0; j < reqNum; j++) {casTest2.addValueByAtomic();}} finally {countDownLatch2.countDown();}}).start();}countDownLatch2.await();end = System.currentTimeMillis();System.out.println("Atomic用时:" + (end - start) + "毫秒,最终值:" + casTest2.atomicLong.get());}
}

Synchronized用时:31110毫秒,最终值:500000000
Atomic用时:27767毫秒,最终值:500000000

可以看出Atomic 效率高一些

正是因为Synchronized这种重量级锁的性能较低,才会有CAS的诞生,那什么是CAS呢?

2、什么是CAS

cas(Compare and swap),中文翻译为比较并交换,这是实现并发算法时常用到的一种技术。

它包含三个操作数:内存位置、预期原值和要更新的值。

执行CAS操作的时候,将内存位置的值与预期原值比较:

如果相匹配,那么处理器会自动将该位置的值更新为新值,

如果不匹配,处理器不做任何处理,多个线程同时执行CAS操作,只会有一个成功。

CAS只是一种算法思想,在Java中落地为原子类,也即是java.util.concurrent.atomic包中的类

 简单使用:

        //1、初始化AtomicInteger 并赋初始值为5AtomicInteger atomicInteger = new AtomicInteger(3);//2、第一次比较并交换   期望值是3,如果和内存位置中的值是一致的,则改为6System.out.println(String.format("第一次比较并交换结果:%s,执行后atomicInteger值为:%s",atomicInteger.compareAndSet(3, 6), atomicInteger.get()));//3、第二次比较并交换   期望值还是3,因为内存值不为3,所以交换返回falseSystem.out.println(String.format("第一次比较并交换结果:%s,执行后atomicInteger值为:%s",atomicInteger.compareAndSet(3, 6), atomicInteger.get()));

第一次比较并交换结果:true,执行后atomicInteger值为:6
第一次比较并交换结果:false,执行后atomicInteger值为:6

源码解释,其他重载方法原理是一致的。

 

3、为什么CAS性能会比Synchronized

CAS是JDK提供的非阻塞原子性操作,它通过硬件保证了比较-更新的原子性。

CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,通过跟踪代码会发现CAS底层使用的都是Unsafe类,Unsafe提供的CAS方法底层实现即为CPU指令cmpxchg。

执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行cas操作,也就是说CAS的原子性实际上是CPU实现独占的,比起用Synchronized重量级锁,这里的排他时间要短很多,所以在多线程情况下性能会相对较好。

4、自旋锁

CAS是实现自旋锁的基础,CAS利用CPU指令保证了操作的原子性,以达到锁的效果。

自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用自旋方式去尝试获取锁,当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。这样的好处是减少了由于线程切换带来的消耗,缺点也是非常明显,不断循环会消耗CPU。在AtomicInteger中有这样的方法如下:即为通过CAS+循环实现自旋锁

/* Atomically updates the current value with the results of* applying the given function, returning the previous value. The* function should be side-effect-free, since it may be re-applied* when attempted updates fail due to contention among threads. @param updateFunction a side-effect-free function* @return the previous value* @since 1.8*/
public final int getAndUpdate(IntUnaryOperator updateFunction) {int prev, next;do {prev = get();next = updateFunction.applyAsInt(prev);// 判断prev和内存中值是否一致,如果一致则更改并返回true,否则一直循环} while (!compareAndSet(prev, next));return prev;
}

面试题:以CAS为基础实现一个自旋锁

package com.lc.test04;import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;/* @author liuchao* @date 2023/4/16*/
public class CasTest01 {AtomicReference<Thread> atomicReference = new AtomicReference<>();/* 加锁*/public void lock() {Thread thread = Thread.currentThread();System.out.println(thread.getName() + "进入加锁");//如果当前atomicReference变量中的值为空,说明无线程使用,则当前线程占用加锁成功while (!atomicReference.compareAndSet(null, thread)) {}}/* 解锁*/public void unlock() {Thread thread = Thread.currentThread();//如果当前atomicReference变量中的值为当前线程,则将内存值设置为null,让别的线程使用,解锁成功atomicReference.compareAndSet(thread, null);System.out.println(thread.getName() + "解锁");}public static void main(String[] args) {CasTest01 cas = new CasTest01();new Thread(() -> {cas.lock();try {//模拟业务执行5秒TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {throw new RuntimeException(e);} finally {cas.unlock();}}, "t1").start();//让线程t1 先于t2 执行try {TimeUnit.MILLISECONDS.sleep(5);} catch (InterruptedException e) {throw new RuntimeException(e);}new Thread(() -> {cas.lock();try {//模拟业务执行5秒TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {throw new RuntimeException(e);} finally {cas.unlock();}}, "t2").start();}}

t1进入加锁
t2进入加锁
t1解锁
t2解锁

5、CAS缺点

两个问题:

  • 循环时间长开销大

在并发线程数少的时候看不出来,随着线程数的不断增大,因为只有一个线程能拿到锁其他线程一直在自璇,会明显感觉出来,所以在高并发场景不建议使用CAS。

  • ABA问题

CAS算法实现的一个重要前提是需要取出内存中某个时刻的数据并在当下时刻比较并替换,那么在这个时间差内会数据可能从A变成B,然后又变成A了,这个时候进行CAS操作会发现内存中任然是A,期望值和内存值是一致的,然后就操作成功。 

尽管CAS操作成功了,但是不代表这个过程就是没有问题的。

举个现实中的问题,我们看过很多影视片中,银行每隔一段时间都要比对一下账户的钱是正确,假如是3个月,那财务主管在第一次比对账户后,将银行钱挪用100万,然后等下次比对账户之前再填充上去,第二次比对账户的时候钱总额没有问题,但是中间却出现了公款私用问题。

那要怎么解决此问题呢?

为了解决此问题引入了AtomicStampedReference类,此类针对每次修改都会将版本号加1,使用如下

       /* 参数1:初始化值* 参数2:初始化版本号*/AtomicStampedReference<Long> atomicStampedReference = new AtomicStampedReference<>(1L, 1);System.out.println(String.format("初始值:%s,初始版本号:%s", atomicStampedReference.getReference(), atomicStampedReference.getStamp()));/* 比较并设置* 参数1:期望值* 参数2:要修改为的值* 参数3:期望版本号* 参数4:要修改的版本号*/boolean result = atomicStampedReference.compareAndSet(1L, 2L, atomicStampedReference.getStamp(),atomicStampedReference.getStamp() + 1);System.out.println(String.format("第一次执行结果:%s,值:%s,版本号:%s", result, atomicStampedReference.getReference(), atomicStampedReference.getStamp()));/* 中间又将值改成初始值了*/result = atomicStampedReference.compareAndSet(2L, 1L, atomicStampedReference.getStamp(),atomicStampedReference.getStamp() + 1);System.out.println(String.format("第二次执行结果:%s,值:%s,版本号:%s", result, atomicStampedReference.getReference(), atomicStampedReference.getStamp()));

初始值:1,初始版本号:1
第一次执行结果:true,值:2,版本号:2
第二次执行结果:true,值:1,版本号:3