《Java-SE-第二十八章》之CAS

简介: 《Java-SE-第二十八章》之CAS

文章目录

CAS

什么是CAS?

CAS是怎么实现的

CAS的应用

1. 原子类

2. 实现自旋锁

CAS的ABA问题

什么是ABA问题

ABA问题引来的BUG

ABA问题复现

解决方案

CAS

什么是CAS?

CAS: 全称Compare and swap,字面意思:”比较并交换“,一个 CAS 涉及到以下操作:把内存中的某个值和CPU寄存器A中的值,进行比较,如果两个值相同,就把另一个寄存器B中的值个内存的值进行交换,也就是把内存的值放到寄存器B,同时把寄存器B的值写给内存。

CAS 伪代码如下:

boolean CAS(address, expectValue, swapValue) {
 if (&address == expectedValue) {
   &address = swapValue;
        return true;
   }
    return false;
}

上述伪代码看起来是线程不安全的,实际上是安全的,因为上述 操作都是硬件上提供的原子性的指令完成的。当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。

CAS是怎么实现的

 针对不同的操作系统,JVM 用到了不同的 CAS 实现原理,简单来讲:java 的 CAS 利用的的是 unsafe 这个类提供的 CAS 操作;unsafe 的 CAS 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg;Atomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 cpu 硬件提供的 lock 机制保证其原子性。简而言之,是因为硬件予以了支持,软件层面才能做到。

CAS的应用

1. 原子类

 Java标准库中提供了 java.util.concurrent.atomic 包, 里面的类都是基于这种方式来实现的,这些类名都以Atomic开头,针对基础的数据类型进行封装,由于是基于CAS实现的,所以都是线程安全的。

以 AtomicInteger 举例,常见方法有

addAndGet(int delta); i += delta;

decrementAndGet(); --i;

getAndDecrement(); i–;

incrementAndGet(); ++i;

getAndIncrement(); i++;

使用演示

两个线程对同一个变量,各自自增5000,使其达到一万,这次不加锁,使用AtomicInteger 来实现

代码如下:

import java.util.concurrent.atomic.AtomicInteger;
public class AtomicDemo {
    private static AtomicInteger counter = new AtomicInteger();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i <5000;i++) {
                counter.getAndIncrement();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i <5000;i++) {
                counter.getAndIncrement();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter);
    }
}

运行结果:

getAndIncrement ()的伪代码实现

class AtomicInteger {
    private int value;
    public int getAndIncrement() {
        int oldValue = value;
        while ( CAS(value, oldValue, oldValue+1) != true) {
            oldValue = value;
       }
        return oldValue;
   }
}

CAS中的i++

假设两个线程同时调用 getAndIncrement

(1) 两个线程都读取 value 的值到 oldValue 中

(2)线程1 先执行 CAS 操作. 由于 oldValue 和 value 的值相同, 直接进行对 value 赋值(value=value+1)

(3)线程2 再执行 CAS 操作, 第一次 CAS 的时候发现 oldValue 和 value 不相等, 不能进行赋值. 因此需要进入循环. 在循环里重新读取 value 的值赋给 oldValue

(4)线程2 接下来第二次执行 CAS, 此时 oldValue 和 value 相同, 于是直接执行赋值操作.

5.(线程1 和 线程2 返回各自的 oldValue 的值即可.

2. 实现自旋锁

此外基于CSA还可以实现自旋锁

伪代码如下:

public class SpinLock {
    private Thread owner = null;
    public void lock(){
        // 通过 CAS 看当前锁是否被某个线程持有. 
        // 如果这个锁已经被别的线程持有, 那么就自旋等待. 
        // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程. 
        while(!CAS(this.owner, null, Thread.currentThread())){
       }
   }
    public void unlock (){
        this.owner = null;
   }
}

 上述代码逻辑,如果当前锁对象被线程占用,则lock()方法会 不断的获取锁是否释放,一旦释放了就将owner置为null,然后根据CAS操作将占用该锁的线程设置为当前的线程,并日退出lock()方法,如果是要解锁,就将占用锁对象的线程设置为null。

CAS的ABA问题

什么是ABA问题

 通过上述介绍了CAS的操作,该操作最主要的就是先比较,满足条件后交换。但是这存在一个非常极端的情况,假设有2个线程t1和t2,有一个共享变量num,初始值为A,接下里,线程t1想使用CAS把num值修改为Z,由于需要进行CAS操作,就需要先读取num的值,保存到oldNum中m,然后CAS判断当前的num的值是否为A,如果是A,就修改成Z。但是t1执行这两个操作之间,t2线程可能把num的值从A修改成B,又从B修改成A。而线程t1中的CAS的期望啥num的值不变就修改,但是num被t2线程修改了,只不过又改回来了,此时t1是无法判断当前的这个变量始终是A,还是经历了一个变化的过程,那么是否要更新num的值为Z呢。这就是ABA问题,举个栗子,来记忆一下,张三和李四是一对情侣,某一天闹掰了,就分手了,张三在分手期间又找了个女朋友,过了半年又和新的女朋友分手了和李四又在一起了,这个过程中,李四是不知道张三已经移情别恋了。

ABA问题引来的BUG

假设张三有100存款,张三想去ATM取50块钱,取款机创建了2个线程来并发执行这个操作。我们期望一个线程执行成功,另一个线程执行失败,如果使用CAS的方式来来完成这个扣款的过程就会出bug。

正常的过程

1.存款 100. 线程1 获取到当前存款值为 100, 期望更新为 50; 线程2 获取到当前存款值为 100, 期望更新为 50

2.线程1 执行扣款成功, 存款被改成 50. 线程2 阻塞等待中.

3.轮到线程2 执行了, 发现当前存款为 50, 和之前读到的 100 不相同, 执行失败.

异常的过程

1.存款 100. 线程1 获取到当前存款值为 100, 期望更新为 50; 线程2 获取到当前存款值为 100, 期望更新为 50.

2.线程1 执行扣款成功, 存款被改成 50. 线程2 阻塞等待中.

3.在线程2 执行之前, 滑稽的朋友正好给滑稽转账 50, 账户余额变成 100 !!

4.轮到线程2 执行了, 发现当前存款为 100, 和之前读到的 100 相同, 再次执行扣款操作

此时ATM就会扣款2次,预期结果是扣一次50,结果扣了2次。

ABA问题复现
public class AtomicReferenceDemo {
    public static AtomicReference<String> ref = new AtomicReference<String>("A");
    public static void main(String[] args) throws InterruptedException {
        System.out.println("main start ...");
        String prev = ref.get();
        update();
        Thread.sleep(1000);
        System.out.println("change A->Z "+ref.compareAndSet(prev, "Z"));
    }
    private static void update() throws InterruptedException {
        new Thread(() -> {
            System.out.println("change A-B");
            ref.compareAndSet(ref.get(), "B");
        },"t1").start();
        new Thread(() -> {
            System.out.println("change B-A");
            ref.compareAndSet(ref.get(), "A");
        },"t2").start();
    }
}

运行结果:

解决方案 给要修改的值, 引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期,CAS 操作在读取旧值的同时, 也要读取版本号,真正修改的时候, 如果当前版本号和读到的版本号相同, 则修改数据, 并把版本号 + 1.如果当前版本号高于读到的版本号. 就操作失败(认为数据已经被修改过了).。

在 Java 标准库中提供了 AtomicStampedReference 类. 这个类可以对某个类进行包装, 在内部就提供了上面描述的版本管理功能.

AtomicStampedReference使用演示

代码如下

import java.util.concurrent.atomic.AtomicStampedReference;
public class AtomicStampedReferencedDemo {
    static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
    public static void main(String[] args) throws InterruptedException {
        System.out.println("main start ...");
        //获取A的值
        String prev = ref.getReference();
        //获取版本号
        int stamp = ref.getStamp();
        System.out.println(stamp);
        update();
        Thread.sleep(1000);
        System.out.println("change A->Z "+ref.compareAndSet(prev, "Z",stamp,stamp+1));
    }
    private static void update() throws InterruptedException {
        new Thread(() -> {
            System.out.println("change A-B");
            ref.compareAndSet(ref.getReference(), "B",ref.getStamp(), ref.getStamp()+1);
            System.out.println("t1的版本号:"+ref.getStamp());
            },"t1").start();
        new Thread(() -> {
            System.out.println("change B-A");
            ref.compareAndSet(ref.getReference(), "A",ref.getStamp(), ref.getStamp()+1);
            System.out.println("t2的版本号:"+ref.getStamp());
        },"t2").start();
    }
}

运行结果:


各位看官如果觉得文章写得不错,点赞评论关注走一波!谢谢啦!。

相关文章
|
Java 编译器
解密Java多线程中的锁机制:CAS与Synchronized的工作原理及优化策略
解密Java多线程中的锁机制:CAS与Synchronized的工作原理及优化策略
|
2月前
|
安全 Java API
JAVA并发编程JUC包之CAS原理
在JDK 1.5之后,Java API引入了`java.util.concurrent`包(简称JUC包),提供了多种并发工具类,如原子类`AtomicXX`、线程池`Executors`、信号量`Semaphore`、阻塞队列等。这些工具类简化了并发编程的复杂度。原子类`Atomic`尤其重要,它提供了线程安全的变量更新方法,支持整型、长整型、布尔型、数组及对象属性的原子修改。结合`volatile`关键字,可以实现多线程环境下共享变量的安全修改。
|
3月前
|
安全 Java 调度
解锁Java并发编程高阶技能:深入剖析无锁CAS机制、揭秘魔法类Unsafe、精通原子包Atomic,打造高效并发应用
【8月更文挑战第4天】在Java并发编程中,无锁编程以高性能和低延迟应对高并发挑战。核心在于无锁CAS(Compare-And-Swap)机制,它基于硬件支持,确保原子性更新;Unsafe类提供底层内存操作,实现CAS;原子包java.util.concurrent.atomic封装了CAS操作,简化并发编程。通过`AtomicInteger`示例,展现了线程安全的自增操作,突显了这些技术在构建高效并发程序中的关键作用。
69 1
|
6月前
|
算法 安全 Java
Java多线程基础-12:详解CAS算法
CAS(Compare and Swap)算法是一种无锁同步原语,用于在多线程环境中更新内存位置的值。
68 0
|
4月前
|
安全 Oracle Java
(四)深入理解Java并发编程之无锁CAS机制、魔法类Unsafe、原子包Atomic
其实在我们上一篇文章阐述Java并发编程中synchronized关键字原理的时候我们曾多次谈到过CAS这个概念,那么它究竟是什么?
|
6月前
|
安全 Java 编译器
Java 多线程系列Ⅴ(常见锁策略+CAS+synchronized原理)
Java 多线程系列Ⅴ(常见锁策略+CAS+synchronized原理)
|
6月前
|
算法 Java
Java中CAS算法的集中体现:Atomic原子类库,你了解吗?
【5月更文挑战第15天】Java中CAS算法的集中体现:Atomic原子类库,你了解吗?
54 1
|
5月前
|
安全 Java
并发编程-Java如何实现原子操作(CAS或锁)
并发编程-Java如何实现原子操作(CAS或锁)
35 0
|
6月前
|
存储 安全 Java
【Java EE】CAS原理和实现以及JUC中常见的类的使用
【Java EE】CAS原理和实现以及JUC中常见的类的使用
|
6月前
|
Java 程序员 C++
Java中CAS详解
Java中CAS详解
52 0