Java EE 多线程之 CAS

1. 什么是 CAS

CAS:全称Compare and swap


假如有一个内存 M
有两个寄存器 A B
CAS(M, A, B)
如果 M 和 A 的值相同的话,就把 M 和 B 里的值进行交换,同时整个操作返回 true
如果 M 和 A 的值不同的话,无事发生,同时整个操作返回 false

这里交换的本质,是为了把 B 赋值给 M
寄存器 B 的值不太关心,主要的是 M 里面的情况


CAS 伪代码

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

上⾯写的代码不是原⼦的,真实的 CAS 是⼀个原⼦的硬件指令完成的,这个伪代码只是辅助理解 CAS 的⼯作流程

CAS 其实是一个 cpu 指令
单个的 cpu 指令,是原子的,就可使用 CAS 完成一些操作,进一步替代“加锁”

基于 CAS 实现线程安全的方式,也称为“无锁编程”
优点
保证线程安全,同时避免阻塞(提高效率)
缺点
1.代码会更复杂,不好理解
2.只能够适合一些特定场景,不如加锁方式更普适

CAS 本质上是 cpu 提供的指令;后来又被操作系统封装,提供成 api;然后又被 JVM 封装,也提供成 api;最后被程序员使用

2. CAS 有那些应用

2.1 实现原子类

在前面进行多线程编写的时候,我们说过int++
int,进行++,不是原子的(load,add,save)

但是标准库中提供了一个类,就是原子的
AtomicInteger,就是基于CAS 的方式对 int 进行封装,此时进行 ++,就是原子的了

import java.util.concurrent.atomic.AtomicInteger;

public class ThreadDemo34 {
        // 不使用原生的 int, 而是替换成 AtomicInteger
        // private static int count = 0;
        private static AtomicInteger count = new AtomicInteger(0);

        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(() -> {
                for (int i = 0; i < 50000; i++) {
                    // count++;
                    count.getAndIncrement();
                    // ++count;
                    //count.incrementAndGet();
                    // count += n;
                    //count.getAndAdd(n);
                    // count--;
                    //count.getAndDecrement();
                    // --count;
                    //count.decrementAndGet();
                }
            });

            Thread t2 = new Thread(() -> {
                for (int i = 0; i < 50000; i++) {
                    count.getAndIncrement();
                }
            });

            t1.start();
            t2.start();

            t1.join();
            t2.join();

            System.out.println("count = " + count.get());
        }

}

在这里插入图片描述
这个时候就是线程安全的了


我们可以查看一下原码
在这里插入图片描述
在java 中,有些操作是偏底层的操作,偏底层的操作早使用的时候就会有更多的注意事项
稍有不慎就容易写出问头
这些操作,就放到了 unsafe 中进行归类
在这里插入图片描述
在原子类的内部没有使用 synchronized 加锁
在这里插入图片描述
再内部就无法看到了
native 修饰的方法,称为“本地方法”
就是在 JVM 源码中,使用 c++ 实现的逻辑
这就涉及到一些底层操作

结论:原子类里面是基于 CAS 来实现的
在这里插入图片描述

2.2 实现自旋锁

自旋锁伪代码

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

private Thread owner 记录当前这个锁被那个线程获取到了,如果是 null,表示未加锁状态

2.3 ABA 问题

CAS 进行操作的关键,是通过 值“没有发生变化” 来作为 “没有其他线程穿插执行” 的判定依据

但是这种判定方式,不够严谨
在更极端的情况下,可能有另一个线程穿插进来,把值从 A 改成 B,再从 B 改成 A
针对以一个线程来说,看起来好像是这个值没有改变,但是实际上已经穿插执行了
这就是 ABA 问题

ABA 问题如果真的出现了,其实大部分情况下也不会产生 bug ,虽然另一个线程穿插执行了,由于值又改回去了,此时逻辑上也不一定会产生 bug

ABA 问题通常不会有 bug,但是极端情况下可能会出现问题
假设一个场景,我去 ATM 取钱,我本身的账户有 1000
我需要去 500,但是发生了 bug
安全取钱按钮,没有发生反应,又按了一下此时就产生了两个线程进行扣款操作
在这里插入图片描述

2.3.1 ABA 问题的解决方案

只要让判定的数值,按照一个方向增长即可(不要反复)
如果有增有减,就可能出现 ABA
只是增加,或者只是减少,就不会出现ABA

但是像账户余额这样的概念,本身就应该能增能减
可以引入一个额外的变量,版本号
约定每次修余额,都要让版本号自增

此时在使用 CAS 判定的时候,就不是直接判定余额了,而是判定版本号,看版本号是否是变化的了
如果版本号不变,注定没有线程穿插执行了

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柒柒要开心

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值