[线程与网络] 多线程(八):CAS问题

🌸个人主页:https://blog.csdn.net/2301_80050796?spm=1000.2115.3001.5343
🏵️热门专栏:🍕 Collection与数据结构 (90平均质量分)https://blog.csdn.net/2301_80050796/category_12621348.html?spm=1001.2014.3001.5482
🧀Java EE(94平均质量分) https://blog.csdn.net/2301_80050796/category_12643370.html?spm=1001.2014.3001.5482
🍭MySql数据库(93平均质量分)https://blog.csdn.net/2301_80050796/category_12629890.html?spm=1001.2014.3001.5482
感谢点赞与关注~~~
在这里插入图片描述

1. CAS问题

1.1 什么是CAS

CAS全程compare and swap,字面意思就是比较和交换(与其说是交换,不如说是赋值),CAS是一种乐观锁的策略,用于实现多线程环境下的数据同步。
线程会涉及到以下操作:

底层中涉及到内存中的共享数据和两个寄存器,还有若干修改这个共享数据的线程.

  1. 首先读取数据,把数据从内存中读取到寄存器1中,之后对这个值进行一系列操作之后保存到寄存器2中.
  2. 之后比较,把内存地址中的值和寄存器1中的值进行比较,看看是否相等.
  3. 如果相等,把寄存器2中的值赋值到内存的共享数据中.
  4. 如果不相等,说明有其他的线程修改过内存中的值,当前线程会尝试重新获取内存中的数据到寄存器1中,之后重复上述的步骤,或者不进行任何操作.
  5. 返回值为是否赋值成功.
    注意:上述的操作为原子操作.

1.2 伪代码

注意:下面的代码不是原子的,只是用于辅助理解CAS的过程,实际的CAS问题在硬件底层中是原子的.

boolean CAS(address,expectValue,swapValue){//address内存中的值,expectValue线程1,swapValue线程2
	if(address == expectValue){
		address = swapValue;
		return true;
	}
	return false;
}

1.3 CAS是如何实现的

简而言之,是因为硬件方面提供了支持,软件层面才可以做到.由CPU提供了CAS对应的硬件指令,因此操作系统内核也能够完成这样的操作,之后OS会提供出响应的api,JVM对OS提供出的api进行封装,我们便可以在Java中使用CAS.

1.4 CAS的有哪些应用

1.4.1 实现原子类

标准库中提供了java.util.concurrent.atomic 包,里面的类都是基于这种方式来实现的.
我们之前提到过原子性,我其中不保证线程安全的其中一个原因就是不保证原子性.
原子性是指一个操作是不可中断的,即使是在多线程环境下,一个原子操作一旦开始,就不会被其他线程干扰,直到这个操作完成。
而CAS问题就可以保证对一个变量操作的原子性,所以就可以使用CAS来实现原子类.
典型的就是AtomicInteger类.其中的getAndIncrement相当于i++操作.

  • 该类的构造方法可以指定一开始变量的初始值.
  • increamentAndGet --> ++i
  • getAndIncrement–> i++
  • decreamentAndGet --> --i
  • getAndDecreament --> i–
  • getAndAdd(10) --> i+=10
import java.util.concurrent.atomic.AtomicInteger;

public class Demo26 {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger atomicInteger = new AtomicInteger(0);
        Thread thread = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                atomicInteger.incrementAndGet();
            }
        });
        Thread thread1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                atomicInteger.incrementAndGet();
            }
        });
        thread.start();
        thread1.start();
        thread.join();
        thread1.join();
        System.out.println(atomicInteger);
    }
}

运行结果:
在这里插入图片描述
我们拿其中的getAndIncrement操作来解释CAS问题是如何在原子类中执行的.

  1. 两个线程都读取value的值到oldValue中.
    在这里插入图片描述
  2. 线程1先进行CAS操作.由于oldValue和value的值相同,直接进行对value赋值.
    在这里插入图片描述
  3. 线程2再执行CAS操作,第⼀次CAS的时候发现oldValue和value不相等,不能进行赋值.因此需要进入循环.在循环里重新读取value的值赋给oldValue.
    在这里插入图片描述
  4. 线程2接下来第二次执行CAS,此时oldValue和value相同,于是直接执行赋值操作.
    在这里插入图片描述
  5. 线程1和线程2返回各自的oldValue的值即可.

伪代码实现

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

总地来说,原子类使用的是乐观锁,加锁操作相对轻量,使得代码的效率更高,但是这种操作只适用于部分场景,使用同步加锁的场景还是比原子类更加通用.

1.4.2 实现自旋锁

自旋锁的解释见下一篇文章.
伪代码实现:
首先我们需要一个owner变量来标记当前持有锁的线程,之后当前线程就会把null和owner进行比较,如果相等,则会把当前线程的值赋值给owner,当前线程就获取到了这把锁,如果不相等,则证明其他线程正在持有锁,当前线程进入忙等状态,会不断把null和owner做比较,直到相等为止,相等之后CAS机制立马就会把当前线程的对象的值赋给owner.

public class SpinLock{
	private Thread owner = null;
	public void lock(){
		while(!CAS(owner,null,Thread.currentThread()){//判断锁是否被占用,没有就使用当前线程赋值
		}
	}
	public void unlock(){
		this.owner = null;//解锁之后赋值为null
	}
}

1.5 CAS的ABA问题

1.5.1 什么是ABA问题(值从A变为B又变为A)

现在存在两个线程,t1线程和t2线程,t1线程要想进行CAS,需要进行以下操作:

  • 先读取num的值,记录到oldNum变量中.
  • 使用CAS判定当前num的值是否为A,如果为A,就修改成进行一列操作之后的值,Z.
    如果有一个线程t2在这个中间对当前num的值进行了修改.只不过就是从A改成了B又一次改回了A.这时候,在t1线程判断与A值是否是相同的时候,他就会认为这个值和A是相等的,其实这个值在中间被t2线程修改过,就会产生ABA问题.
    在这里插入图片描述

举例说明:翻新机
这就好比你买来一个新手机,你无法判断这是一个全新的一手手机,还是有一些无良商家对二手机进行了翻新再卖给你.

1.5.2 ABA问题带来的bug

大部分情况下,ABA问题不会造成什么bug,但是不排除出现一些特殊情况会出现bug.

举例说明:钟离去银行取钱
有请助教:钟离,达达利亚

  • 正常的过程:
    t1线程希望从总结金额100中扣款50,这时候t2线程也希望如此,假设t2先进行了CAS操作,t1阻塞等待,t2线程的CAS操作把new值赋值为old-50之后,new值就变为50.之后在t1进行CAS操作的时候,t1就发现new值和old值不一样,就不会进行CAS操作.
    在这里插入图片描述
  • 异常的情况
    在钟离取款的时候,由于达达利亚考虑到钟离每次都可能不带钱,所以在钟离取款的时候,达达利亚又给钟离打了50块钱.在t2线程执行完CAS操作之后,t1还没有执行CAS,这时候t3进行了打款操作,t1进行CAS操作的时候,认为old值和new值相等,所以t1也进行了一次扣款操作.这时候就产生了bug.
    在这里插入图片描述

1.5.3 解决方案

解决ABA问题的这种策略是一种乐观锁的策略,就是引入版本号.
要给修改的值引入版本号.版本号只加不减,每次操作一次余额之后,版本号+=1.在CAS比较数据当前值和旧值的同时,也要比较版本号是否符合预期.

  • 在CAS操作在读取旧值的时候,也要读取版本号.
  • 在真正修改的时候:
    • 如果当前版本号和读到的版本号一样的时候,修改数据.
    • 如果当前版本号高于读到的版本号的时候,就操作失败了.

继续拿前面的钟离取款的例子来说明:
在t1线程进行CAS的时候,进行了扣款操作,版本号+1,之后t3线程进行打款,对版本号+1,之后在t2线程CAS的时候,发现当前版本号高于之前读取到的版本号,则操作失败.
在这里插入图片描述

评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值