CAS 的使用场景 & CAS的ABA问题的优化 以及 synchronized 的三大优化

文章详细介绍了CAS(Compare-And-Swap)的概念和应用场景,如实现原子类和自旋锁,并讨论了CAS的ABA问题及其解决方案。同时,文章探讨了synchronized的优化策略,包括锁升级、锁消除和锁粗化,展示了JVM如何在不同竞争情况下优化锁的性能。
摘要由CSDN通过智能技术生成

目录

🎈专栏链接:多线程相关知识详解 

一.什么是CAS

二.CAS最常用的两个场景

Ⅰ.实现原子类

Ⅱ.实现自旋锁

三.CAS的ABA问题

四.优化解决CAS的ABA问题

五.synchronized的优化 

Ⅰ.锁升级/锁膨胀

Ⅱ.锁消除

Ⅲ.锁粗化


一.什么是CAS

CAS(Compare-And-Swap)(即比较和交换),它是一条CPU并发原语,用于判断内存中某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的.

它包含了三个参数:V,A,B。
V表示要读写的变量内存地址

A表示旧的预期值

B表示准备设置的新值

把内存中的某个值,和CPU寄存器A中的值,进行比较~ 如果两个值相同,就把CPU中另一个寄存器B的值和内存的值进行交换(即把内存的值写到寄存器B,同时把B的值写到内存里),是通过一个CPU指令完成的,是原子的,同时还高效(不涉及到锁冲突,线程等待)

二.CAS最常用的两个场景

Ⅰ.实现原子类

因为count++在多线程的环境下,线程是不安全的,想要线程安全就得加锁----而加锁会导致性能大打折扣,可以基于CAS实现"原子"的++操作,从而保证线程安全&高效

例如:AtomicInteger类

下面这些为AtomicInteger的伪代码

class AtomicInteger{
    private int value;

    public int getAndIncrement(){//相当于count++
        int oldValue = value;//此次oldValue相当于是寄存器A,是把内存value值读取到寄存器里
        //下面这个CAS 是比较看value这个内存中的值,是否适合寄存器A的值相同,如果相同,就把寄存器B里的值给设置到value中,同时CAS返回true,结束循环;如果不相同,则无事发生,CAS返回false,进入循环,在循环体里面,重新读取内存里的值到寄存器A中
        while ( CAS(value,oldValue,oldValue+1) != true){//此次的oldValue+1 把它理解为另一个寄存器B的值
            oldValue = value;
        }
        return oldValue;
    }
}

 AtomicInteger类的原子++使用:

import java.util.concurrent.atomic.AtomicInteger;

public class Demo3 {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger count = new AtomicInteger(0);
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                count.getAndIncrement();//相当于原子类的count++
            }
        });

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

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

        t1.join();
        t2.join();
        System.out.println(count.get());
    }
}

运行结果:

在AtomicInteger类当中还有原子的--操作等等.

Ⅱ.实现自旋锁

自旋锁是纯用户态轻量级锁也是一个乐观锁,当发现锁被其他线程持有的时候,就不会挂起等待,而是会反复询问,看当前的锁是否被释放了

反复询问就是为了抢占执行,节省了进入内核和系统调度的开销

自旋锁是属于消耗CPU资源来换取第一时间获取到锁,如果当前预期锁竞争不太激烈的时候(预期短时间内获取到锁),使用自旋锁就非常合适了

以下为CAS实现自旋锁的伪代码

public class SpinLock {
    private Thread owner = null;
    public void lock(){
        // 通过 CAS 看当前锁是否被某个线程持有. 
        // 如果这个锁已经被别的线程持有, 那么就自旋等待. 
        // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程. 
        while(!CAS(this.owner, null, Thread.currentThread())){
            //比较owner和null是否相同(是否是解锁的状态)
            //如果是就要进行交换,把当前调用lock的线程的值,设置到owner里面,相当于加锁成功,结束循环
            //如果owner不为null,CAS就不会进行交换,返回false,重新进入循环,重新进行判定
       }
   }
    public void unlock (){
        this.owner = null;
   }
}

三.CAS的ABA问题

面试的时候谈到CAS就大概率会问ABA问题

什么是CAS的ABA问题呢?

在CAS中,进行比较的时候,会发现寄存器A和内存M的值相同,但是无法判定是M始终没变,还是M变了,又重新变回来了

举个例子:

当你要去银行取钱的时候,里面有1000存款,想要从ATM中取出500存款,结果取钱的时候机器卡了以下,你多按了几下取钱操作,ATM就创建了多个线程来进行扣款操作,并且该扣款操作是基于CAS来实现的

此时并不会出现大问题,可是如果多了个线程t3,在t2还没进行CAS的时候给我汇款了500

 就会出现重复扣款的情况

四.优化解决CAS的ABA问题

只需要多出一个数据来记录内存中数据的变化,就可以解决这个问题了

针对上面这个例子,可以另外搞一个内存,用来记录M的"修改次数"(版本号)[只增不减的]或者是"上次修改时间"[只增不减],通过这个办法解决ABA问题

此时的修改操作,就不是把账户余额读取到 寄存器A中了,CAS比较的也不是账户余额,而是比较版本号/上次修改时间

 比较的时候发现版本号/上次修改时间变了,就知道账户余额发生了其他变化,就不会进行扣款操作了

五.synchronized的优化 

Ⅰ.锁升级/锁膨胀

synchronized的作用是 加锁 ,当两个线程针对同一个对象加锁的时候,就会出现锁竞争,后来尝试加锁的线程就会进行阻塞等待,直到前面一个线程释放了锁

synchronized加锁的具体过程:

Ⅰ.偏向锁

Ⅱ.轻量级锁

Ⅲ.重量级锁

 synchronized是自适应的

如果当前锁竞争并不激烈,则就是以轻量级锁状态来进行工作(自旋锁),能在第一时间拿到锁

如果当前锁竞争比较激烈,则就是以重量级锁状态来进行工作(挂起等待锁),拿到锁没有那么及时,但是节省了CPU的开销

而偏向锁是指必要的时候再加锁,能不加就不加,类似于懒汉模式,给某个线程加了锁之后,在实际执行的过程中并不一定就会真的触发锁竞争

 偏向锁并不是真加锁,只是设置了一个状态

举个例子:

我交了个女朋友A,考虑到未来换女朋友方便,就没有挑明关系,只行情侣之实,并无情侣之名(这就是以一个偏向锁状态)

如果在我和A交往的过程中,冒出来了个男生B,他也在接近A,对我产生了威胁,此时我立刻和A挑明了关系,并官宣,告诫B我是A的男朋友,让他离A远点(这个时候才是真加锁)

如果没有另外的男生B过来,我和A保持这种关系,直到我腻了之后直接和A分开(节省了 确立关系 & 分手 这些开销)

锁升级/锁膨胀:JVM实现synchronized的时候引入的一些优化机制

所以:

无竞争~>偏向锁

有竞争~>轻量级锁

竞争激烈~>重量级锁 

Ⅱ.锁消除

JVM自动判定,发现这个地方的代码,不必加锁,如果你写该处的时候加了锁,就会自动把锁去掉

例如:只有一个线程的时候  /  有多个线程的时候,并不涉及多个线程修改同一个变量,此时写了synchronized,那么它的加锁操作将会被JVM消除掉

synchronized刚开始的时候加的是偏向锁,只是修改标志位,开销并不大,但是能够消除的时候,就不必多这些开销

锁消除也是编译器优化的一种行为,编译器的判定不一定非常准,当判定代码中的锁100%能够消除,那么就会被消除掉,锁消除只是在编译器/JVM有100%把握的时候才会进行的

Ⅲ.锁粗化

锁的粒度:synchronized对应的代码块包含多少代码,包含的代码少,则粒度细;包含的代码多,则粒度粗

锁粗化就是把细粒度的加锁~>粗粒度的加锁

加锁是必要的,但反复的加锁解锁就会带来一些额外的锁竞争

举个例子:

在进行公司工作汇报的时候,是一个人接着一个人来汇报自己最近所做的工作,而不是这个工作汇报好先换别人汇报,然后自己再汇报其他的工作,所以就得把自己所要汇报的工作汇总起来,轮到自己汇报的时候全部工作一起完成汇报

 能够进行锁粗化的只能是对同一个对象加锁的才可以

评论 24
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

山涧晴岚.

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

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

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

打赏作者

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

抵扣说明:

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

余额充值