线程学习(25)-乐观锁方式CAS简介

简介

原先所学的加锁,例如synchronized,ReetrantLock这些加锁方式都是悲观锁,即当前线程对加锁的内容进行操作时,其它的线程只能等待该线程锁住的内容执行完成之后,才可以获得锁,最终执行自己的业务。

而今天所学的cas,是一种乐观锁的机制。cas,全拼是compare and set,即先比较后赋值(cas是个原子操作)。即当线程修改共享变量的时候,并不会像悲观锁一样,控制着不让其它线程修改当前变量,可以这么理解:

1.线程A先获取到了主存值1,拷贝至工作内存

2.线程B获取到主存值1,拷贝至工作内存

3.线程B对工作内存的值1进行修改为2,并在主存中进行赋值

4.线程A修改工作内存值为3,在往主存里试图写时,发现原先预期的值是1,现在变化成2了,不会,从主存中重新拉取值

5.从主存中拉取了2,然后再次修改工作内存 值为 3,在往主存里试图写时,发现预期的值是2,主存现在的值也是2,没有问题,对内容进行写入。

转账例子

就设置一个金额,有两个线程对其进行转账,一个是往出转,另一个往内部转。

加锁方面的代码的话,很简单,直接在转账金额方面的代码加锁就可以了。

package com.bo.threadstudy.six;

import com.bo.threadstudy.two.ReentrantLockTest;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.ReentrantLock;

@Slf4j
public class TransferTest01 {

    public static void main(String[] args) throws InterruptedException {
        Transfer01 transfer01 = new Transfer01(10000);
        int loopNum = 1000;

        //理解为主线程等待两个线程执行完成
        CountDownLatch countDownLatch = new CountDownLatch(2);

        new Thread(() -> {
            for (int i = 0; i < loopNum; i++) {
                transfer01.addPrice(200);
            }
            countDownLatch.countDown();
        }).start();

        new Thread(() -> {
            for (int i = 0; i < loopNum; i++) {
                transfer01.subjectPrice(200);
            }
            countDownLatch.countDown();
        }).start();

        countDownLatch.await();
        //没有线程安全问题,应该是1万
        log.debug("最终转账后的结果"+transfer01.getMoney());


    }

}

@Slf4j
class Transfer01{
    private int price;
    private ReentrantLock lock = new ReentrantLock();

    public Transfer01(int price) {
        this.price = price;
    }

    public int getMoney(){
        return price;
    }

    public void addPrice(int money){
        lock.lock();
        try{
            price = price+money;
        }finally {
            lock.unlock();
        }
    }

    public void subjectPrice(int money){
        lock.lock();
        try{
            if(price < money){
                log.debug("金额不足");
                return ;
            }
            price = price - money;
        }finally {
            lock.unlock();
        }
    }

}

那么用CAS的方式来看看。

package com.bo.threadstudy.six;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;

@Slf4j
public class TransferTest02 {

    public static void main(String[] args) throws InterruptedException {
        Transfer02 transfer02 = new Transfer02(new AtomicInteger(1000));
        int loopNum = 1000;

        //理解为主线程等待两个线程执行完成
        CountDownLatch countDownLatch = new CountDownLatch(2);

        new Thread(() -> {
            for (int i = 0; i < loopNum; i++) {
                transfer02.addPrice(200);
            }
            countDownLatch.countDown();
        }).start();

        new Thread(() -> {
            for (int i = 0; i < loopNum; i++) {
                transfer02.subjectPrice(200);
            }
            countDownLatch.countDown();
        }).start();

        countDownLatch.await();
        //没有线程安全问题,应该是1万
        log.debug("最终转账后的结果"+transfer02.getMoney());


    }

}


/**
 * 这里采用CAS来做,锁的粒度较小,更为轻量,AtomicInteger
 */
@Slf4j
class Transfer02{
    private AtomicInteger price;

    public Transfer02(AtomicInteger price) {
        this.price = price;
    }

    public int getMoney(){
        return price.get();
    }

    public void addPrice(int money){
        while(true){
            int curPirce = price.get();
            if(price.compareAndSet(curPirce,curPirce+money)){
                //金额替换成功,退出循环
                break;
            }
        }


    }

    public void subjectPrice(int money){
        while(true){
            int curPirce = price.get();
            if(curPirce < money){
                log.debug("金额不足");
                break;
            }
            //在这一步如果发生了指令交错,那么会再走一次循环,不会出现问题
            if(price.compareAndSet(curPirce,curPirce-money)){
                break;
            }
        }
    }

}

AtomicInteger,是一个原子整数类型。可以这么理解,他内部的一系列的方法,如果不在线程中使用组合这些方法的情况下,那么这些方法都是线程安全的。

看一下compareAndSet方法是怎么操作的。打个断点看一下,此时预期的值是200800。

 采用idea的debug功能中的setValue对这个预期值进行一下改动。我现在将其改为了201000值。

 此时现在的代码是这样的,price中主存的值改为了201000,但是当前线程中的预期值是200800,修改后的值为200600,不符合规则,也就不会break结束这个循环,而是继续调用循环,重新从主存中获取。

 在源码中,采用了volatile关键字来保证了可以及时写入主存以及读取主存的内容。

 获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。
它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。即一个线程对 volatile 变量的修改,对另一个线程可见。

CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果。

为什么CAS无锁效率高
 

无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。打个比喻
线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,等被唤醒又得重新打火、启动、加速... 恢复到高速运行,代价比较大
但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换

可以理解为,在加锁的情况下,线程之间会出现上下文切换,这个过程会极其的损耗性能。而如果使用CAS的话,所有的线程此时都在执行,没有线程阻塞的现象发生,性能也比较快。但是并不是说CAS没有缺点,举个例子,现在有许多个线程在CAS使用,就有一个可能,某个线程他经历了很多次循环,但是每次都是预期值与主存中的实际值不同,所以只能一直循环重复获取,无法结束,从而占用CPU,损耗性能。而其它的线程,如果线程数量过多,很可能出现线程饥饿的现象,最终性能上不去。

所以,CAS实际应用的场景应该是,线程数量较少,多核CPU的情况,这种情况下,使用CAS可以提升效率。


CAS特点

结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。
CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再
重试呗。
synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想
改,我改完了解开锁,你们才有机会。
CAS 体现的是无锁并发、无阻塞并发,请仔细体会这两句话的意思
因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值