面经手册 —— 谈谈对 CAS 的理解

面经手册 —— 谈谈对 CAS 的理解

题目描述

提问线路: CAS => Unsafe => CAS 底层原理 => 原子更新 => 如何规避 ABA 问题

面试题分析

compareAndSet 怎么用?

import java.util.concurrent.atomic.AtomicInteger;

public class Main {

    /**
     * boolean compareAndSet(int expect,int update)
     * - 如果主内存的值=期待值expect,就将主内存值改为update
     * - 该方法可以检测线程a的操作变量X没有被其他线程修改过,保证了线程安全
     */
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(5);
        System.out.println(atomicInteger.compareAndSet(5,10) + "\t" + atomicInteger); //true    10
        System.out.println(atomicInteger.compareAndSet(5,20) + "\t" + atomicInteger); //false   10
    }

}
public static void main(String[] args) {
    User z3 = new User("z3", 18);
    User l4= new User("l4", 18);
    AtomicReference<User> atomicReference = new AtomicReference<>(z3);
    System.out.println(atomicReference.compareAndSet(z3,l4) + "\t" + atomicReference.get().toString());
    System.out.println(atomicReference.compareAndSet(z3,l4) + "\t" + atomicReference.get().toString());
    //true	com.example.concurrent.cas.User@4554617c
	//false	com.example.concurrent.cas.User@4554617c
}

CAS 底层原理简述

  • Compare-And-Swap,是一条 CPU 并发原语。(原语:操作系统范畴,依赖硬件,不被中断)
  • 功能是判断内存某个位置的值是否为预期值(compare),如果是,就更新(swap),这个过程是原子的(要么成功,要么失败)。
  • CAS 有 3 个操作数,内存值 V ,预期值 A,要更新的值 B。仅当预期值 A == 内存值时,才将内存值 V 修改为 B,否则什么都不做。
  • 自旋:比较并交换,直到比较成功
  • 底层靠 Unsafe 类保证原子性

getAndIncrement 源码解析

java.util.concurrent.atomic.AtomicInteger#getAndIncrement 源码解析(用了CAS保证线程安全)

/**
 * 参数说明:
 * this:AtomicInteger对象
 * valueOffset:对象的内存地址
 * unsafe:sun.misc.Unsafe 类
 * AtomicInteger中变量value使用volatile修饰,保证内存可见
 */
public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

底层调用了 sun.misc.Unsafe 类

/**
 * compareAndSwapInt,即CAS
 * while:如果修改失败,会一直尝试修改,直到成功
 */
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;
}

简述:

  • 调用了 sun.misc.Unsafe#getAndAddInt 方法
  • getAndAddInt 方法使用 CAS 一直循环尝试修改主内存

对 Unsafe 的理解

  • 该类所有方法都是 native 修饰,直接调用底层资源。位于 sun.misc 包中
  • 可以像 C 的指针一样直接操作内存。Java 的 CAS 操作依赖 Unsafe 类的方法

CAS 有哪些缺点?

  • 循环时间长,开销大

    如果 CAS 失败,就一直 while 尝试。如果长时间不成功,可能给 CPU 带来很大的开销

    • 在内存地址 V 中,存储着值为 10 的变量
    • 线程1想把变量的值加一,对线程1来说,预期值为10,要修改的值为11
    • 在线程1提交更新之前,另一个线程2抢先一步,把变量值更新成了11
    • 线程1开始提交更新,期望值不等于实际值,提交失败
    • 线程1重新获取 V 的值,并重新计算想要修改的值。此时期望值为11,要修改的值为12。这个重新尝试的过程被称为自旋。
    • CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去重试更新。
  • 只能保证一个共享变量的原子操作

    如果时多个共享变量,CAS 无法保证原子性,只 能加锁,锁住代码段。

  • 存在 ABA 问题

ABA 问题描述?

  • 线程1从内存位置 V 取出值 A
  • 此时线程2也取出 A ,且线程2做了一次 CAS 将值改为 B,然后又做了一次 CAS 将值改回了 A
  • 此时线程1开始 CAS,发现内存值还是 A,则线程1 操作成功
  • 这个时候实际上 A 值其实已经被其他线程改变过,这与设计思想是不符合的。

如果只在乎结果,ABA 不介意 B 的存在,没什么问题

如果 B 的存在会造成影响,需要通过 AtomicStampReference ,加时间戳解决。

解决思路:每次变量更新的时候,把变量的版本号加1,这样只要变量被某一个线程修改过,版本号就会递增,从而解决了ABA问题

public class ABADemo {
    AtomicStampedReference atomicStampedReference = new AtomicStampedReference<>(100, 1);
}
public class Main2 {
    public static void main(String[] args) {
        // ABAProblem();
        ABADemo abaDemo = new ABADemo();
        new Thread(() -> {
            // 等线程2读到初始版本号的值
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程1在ABA前的版本号:" + abaDemo.atomicStampedReference.getStamp());
            abaDemo.atomicStampedReference.compareAndSet(100, 101, abaDemo.atomicStampedReference.getStamp(),
                    abaDemo.atomicStampedReference.getStamp() + 1);
            abaDemo.atomicStampedReference.compareAndSet(101, 100, abaDemo.atomicStampedReference.getStamp(),
                    abaDemo.atomicStampedReference.getStamp() + 1);
            System.out.println("线程1在ABA后的版本号:" + abaDemo.atomicStampedReference.getStamp());
        }, "1").start();

        new Thread(() -> {
            // 存一下修改前的版本号
            int stamp = abaDemo.atomicStampedReference.getStamp();
            System.out.println("线程2在修改操作前的版本号:" + stamp);
            // 睡1s等线程1执行完ABA
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(abaDemo.atomicStampedReference.compareAndSet
                    (100, 2020, stamp, abaDemo.atomicStampedReference.getStamp() + 1) + "\t" +
                    abaDemo.atomicStampedReference.getReference());
        }, "2").start();
        //线程2在修改操作前的版本号:1
        //线程1在ABA前的版本号:1
        //线程1在ABA后的版本号:3
        //false 100

    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值