CAS无锁自旋解读

CAS自旋锁

悲观者与乐观者的做事方式完全不一样,悲观者的人生观是一件事情我必须要百分之百完全控制才会去做,否则就认为这件事情一定会出问题;而乐观者的人生观则相反,凡事不管最终结果如何,它会先尝试去做,大不了最后不成功。

这就是悲观锁与乐观锁的区别,悲观锁会把整个对象加锁占为自有后才去做操作,乐观锁不获取锁直接做操作,然后通过一定检测手段决定是否更新数据。

底层原理

乐观锁的核心算法便是CAS[Compare And Swap],

1.自旋锁

借助C语言调用CPU底层指令实现的,基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能 ,提供总线锁定缓存锁定两个机制来爆照复杂内存操作的原子性

自旋锁的核心实现,便要依靠另一个核心类Unsafe

2.Unsafe

CAS的核心类就是UnSafe,来源于jdkrt.jar包下的所有方法都是navive修饰的一个类

native方法:java方法是无法直接访问底层系统的,需要通过native方法来访问

Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据,换言之,unsafe的操作直接调用底层资源执行相应任务!

我们需要提供CAS对象,自增偏移量[内存地址]和自增值,即可完成线程安全的自增操作

在这里插入图片描述

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

Unsafe类中getAndAddInt方法

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

var1: cas操作的对象即AtmoticInteger

var2: cas操作对象的内存地址

var4: 需要变动的数值

var5: 根据内存地址偏移量var2找出var1对象在主内存中真实值

用当前对象的值与var5的内存值进行比较:

相同:更新var5+var4,返回true

不相同:继续取值然后再比较,直到更新完成

在这里插入图片描述

总之,CAS是线程安全且高效的

处理逻辑

它涉及到三个操作数内存值、预期值、新值

当且仅当预期值内存值相等之时才将内存值修改为新值

这样处理的逻辑是,首先检查某块内存的值是否和之前读取时的值一样,如不一样则表示期间此内存值已经被别的线程更改过,舍弃本次操作,否则说明期间此内存值作为预期值,执行到某处时线程二决定将新值设置到内存块中,如果线程一在此期间修改了内存块,则通过CAS即可以检测出来,假如检测没问题则线程二将新值赋予内存卡。

缺点

CAS虽然搞笑的解决院子操作,但是仍然存在三大问题

1. ABA问题

因为CAS需要在操作时检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。

解决方法:打标记/添加版本号,来获取最近一次的更新变化

public class AtomicMarkableReferenceTest {
    private final static String A = "A";
    private final static String B = "B";
    private final static AtomicReference<String> ar = new AtomicReference<>(A);

    public static void main(String[] args) {
        new Thread(() -> {
            try {
                Thread.sleep(Math.abs((int) (Math.random() * 100)));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (ar.compareAndSet(A, B)) {
                System.out.println("我是线程1,我成功将A改成了B");
            }
        }).start();
        new Thread(() -> {
            if (ar.compareAndSet(A, B)) {
                System.out.println("我是线程2,我成功将A改成了B");
            }
        }).start();
        new Thread(() -> {
            if (ar.compareAndSet(B, A)) {
                System.out.println("我是线程3,我成功将B改成了A");
            }
        }).start();
    }
}

输出结果:

我是线程2,我成功将A改成了B
我是线程3,我成功将B改成了A
我是线程1,我成功将A改成了B

显然,线程1和线程2,其实有一方做了多余的操作,假设线程1和线程2的处理逻辑是一样的话,就会出大问题

打标记

使用AtomicMarkableReferenceA->B不处理,B->A打标记,线程1再执行A->B时,检测到标记后不执行

public class AtomicMarkableReferenceTest {
    private final static String A = "A";
    private final static String B = "B";
    private final static AtomicMarkableReference<String> ar = new AtomicMarkableReference<>(A, false);

    public static void main(String[] args) {
        new Thread(() -> {
            try {
                Thread.sleep(Math.abs((int) (Math.random() * 100)));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (ar.compareAndSet(A, B, false, true)) {
                System.out.println("我是线程1,我成功将A改成了B");
            }
        }).start();
        new Thread(() -> {
            if (ar.compareAndSet(A, B, false, true)) {
                System.out.println("我是线程2,我成功将A改成了B");
            }
        }).start();
        new Thread(() -> {
            if (ar.compareAndSet(B, A, ar.isMarked(), true)) {
                System.out.println("我是线程3,我成功将B改成了A");
            }
        }).start();
    }
}

输出结果:

我是线程2,我成功将A改成了B
我是线程3,我成功将B改成了A

显然,线程1已经察觉出值发生了变化,就不在做同样的逻辑操作了。

添加版本号

使用AtomicStampedReference,每次再做A->B操作时,根据传递预期的版本号信息来判断是否继续往下执行

private final static String A = "A";
    private final static String B = "B";
    private static AtomicInteger ai = new AtomicInteger(1);
    private final static AtomicStampedReference<String> ar = new AtomicStampedReference<>(A, 1);

    public static void main(String[] args) {
        new Thread(() -> {
            try {
                Thread.sleep(Math.abs((int) (Math.random() * 100)));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (ar.compareAndSet(A, B, 1, 2)) {
                System.out.println("我是线程1,我成功将A改成了B");
            }
        }).start();
        new Thread(() -> {
            if (ar.compareAndSet(A, B, ai.get(), ai.incrementAndGet())) {
                System.out.println("我是线程2,我成功将A改成了B");
            }
        }).start();
        new Thread(() -> {
            if (ar.compareAndSet(B, A, ai.get(), ai.incrementAndGet())) {
                System.out.println("我是线程3,我成功将B改成了A");
            }
        }).start();
    }

输出结果:

我是线程2,我成功将A改成了B
我是线程3,我成功将B改成了A

很明显,已经阻止了ABA情况的发生

2.循环时间长开销大

自旋如果时间太长不成功,会给cpu造成一定的负荷

解决方法:和ABA中添加版本号类似,版本号达到一定次数时,主动停止

3. 只保证一个共享变量原子操作

这意味着CAS在有多个变量值的情况下,是无法保证线程的安全问题

解决方法:使用AtomicReference对象将多个变量值放入到一个对象操作

使用场景

并发冲突少,竞争不激烈的情况下,使用CAS是比较高效的,高并发情况下冲突概率上升,自旋效率较低

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值