首页 > 其他分享 >一文带你了解乐观锁和悲观锁的本质区别!

一文带你了解乐观锁和悲观锁的本质区别!

时间:2024-07-05 18:29:31浏览次数:3  
标签:一文 CAS 线程 本质区别 悲观 debug prev ref log

文章目录

悲观锁是什么?

悲观锁它总是假设最坏的情况,它会认为共享资源在每次被访问的时候就会出现线程安全问题,所以每次在获取资源的时候都会上锁,以避免线程安全问题发生

也就是说,共享资源每次只给一个线程使用,而其他的线程则会阻塞住,当占据锁的线程用完后才会把共享资源释放掉,让给其它线程来进行竞争。

这样就会导致在高并发的场景下容易造成死锁、以及线程阻塞等,增加系统的开销。

乐观锁是什么?

乐观锁总是假设最好的情况,它认为共享资源每次被访问的时不会出现线程问题,所以也就不用加锁去保证线程安全,因此线程可以不停地执行,只有当提交修改的时候去验证对应的共享资源是否被其它线程修改。

高并发的场景下,乐观锁不存在锁竞争造成线程阻塞,也不会有死锁的问题,在性能上往往会更胜一筹。
但是,如果写操作的冲突频繁发生,会频繁失败和重试,这样同样会非常影响性能。

如何实现乐观锁?

什么是CAS

CAS是Compare-And-Swap(比较并交换)的缩写,是一种轻量级的同步机制,主要用于实现多线程环境下的无锁算法和数据结构,保证了并发安全性。它可以在不使用锁的情况下,对共享数据进行线程安全的操作。

它就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。CAS 操作是一个原子操作,它在执行期间不会被其他线程中断。因此,它能够提供一种乐观并发控制机制,避免了传统锁机制的开销和可能的线程阻塞。

它的其实主要就是两个步骤:冲突检测以及数据更新

通常包含三个参数:内存位置(或称为变量)、期望值新值。它的执行步骤如下:
  1. 读取内存位置的当前值。
  2. 检查当前值是否与期望值相等。如果相等,则进行步骤4;如果不相等,则说明其他线程已经修改了该值,操作失败。
  3. 如果当前值与期望值相等,则将新值写入内存位置。
  4. 返回操作是否成功的标志。

class AccountSafe implements Account {
    private AtomicInteger balance; // 原子整数类型 
    public AccountSafe(Integer balance) {
        this.balance = new AtomicInteger(balance);
    }
    @Override
    public Integer getBalance() {
        return balance.get();
    }
    @Override
    public void withdraw(Integer amount) {
        while (true) {
            // 没同步到主存 因为是局部变量,只在线程的工作内存之中
            int prev = balance.get(); // 获取余额最新值
            int next = prev - amount; // 修改后的余额
            // 真正修改
            if (balance.compareAndSet(prev, next)) { 
                // 成功为true;失败false,继续循环 
                break;
            }
        }
    }
}

我们再来仔细看一下withdraw方法

public void withdraw(Integer amount) {
    // 需要不断尝试,直到成功为止
    while (true) {
        // 比如拿到了旧值 1000
        int prev = balance.get();
        // 在这个基础上 1000-10 = 990
        int next = prev - amount;
        /*
             compareAndSet 正是做这个检查,在 set 前,先比较 prev 与 当前值!!!
             当不一致时,next 作废,返回 false 表示失败
             比如,别的线程已经做了减法,当前值已经被减成了990
             那么本线程的这次 990 就作废了,进入 while 下次循环重试
             直到一致,以 next 设置为新值,返回 true 表示成功
         */
        if (balance.compareAndSet(prev, next)) {
            break;
        }
    }
}

在并发环境中,多个线程可以同时执行CAS操作来更新同一个内存位置的值。如果多个线程同时执行CAS操作,只有一个线程的CAS操作会成功,其他线程的操作将失败。在失败的情况下,可以选择重试CAS操作。

应用

  1. JVM创建对象的过程中分配内存【堆中 因为这个是共享 所以要保证安全】
  2. syn轻量级锁的时候,JVM尝试使用CAS操作,将对象头的Mark Word更新为指向锁记录的指针。
  3. ReentrantLock中的非公平锁,也使用CAS来管理锁的状态。比如,尝试获取锁时会使用CAS来检查并更新锁的状态。
  4. 并发集合:如ConcurrentHashMap等,并发集合的实现中也大量使用了CAS操作,以实现高效的线程安全访问。
  5. 原子类:如AtomicIntegerAtomicLongAtomicReference等,这些类提供了一组原子操作,允许你在单个操作中安全地读取、写入和更新变量。这些操作背后就是通过CAS来实现的。

局限性

  1. 只能保证对单个共享变量的操作是原子性的,无法保证对多行代码实现原子性
  2. 高并发场景下,竞争激烈,CAS 失败重试会频繁发生,自旋时间过长,而线程又不阻塞,抢占 CPU 资源,导致 CPU 使用率飙升,反而影响了性能
    a. 指定 CAS 一共循环多少次,如果超过这个次数,直接失败或将线程挂起(参考 synchronized 中的自旋锁) .
    b. 可以通过分段的思想减少竞争,使用原子累加器 LongAdder,当有竞争时设置多个累加单元,最后将结果汇总
  3. ABA问题

ABA问题是什么?

先看例子:

假设你在银行的查看账户余额。第一次查看时,余额显示为100元(状态A)。然后打算取出50元,但在操作之前,出于确认目的,再次检查余额,发现还是100元,似乎没有变化(仍然是状态A)。

但实际情况可能是,在两次查看之间,有人往你的账户存入了50元(状态变为B:150元),然后又立即取出了50元(状态再次回到A:100元)。尽管最终余额回到了初始查看的数值,但实际上账户经历了存取的变化(A->B->A)。

在并发编程的上下文中,这就是“ABA问题”。当你CAS操作来确保数据一致性时,如果仅比较前后值是否相同(都是A),就可能会忽略掉中间发生的改变(B状态),误以为数据从未被改动过,从而可能导致逻辑错误或数据不一致性!

如何解决?

解决ABA问题的一种常见方法是引入版本号或者时间戳,每次修改变量时不仅更新其值,还增加版本号或时间戳。这样,即便值回到了最初的状态,通过检查版本号或时间戳的不同,也可以察觉到变量曾经被修改过。

AtomicStampedReference(维护版本号)
AtomicStampedReference通过捆绑一个引用及其关联的stamp(印记,可以视为版本号或时间戳)来工作,以此增强传统的比较并交换(CAS)操作。

它允许线程在执行 CAS 操作时,不仅检查引用是否发生了变化,还要检查时间戳是否发生了变化。这样,即使一个变量的值被修改后又改回原值,由于时间戳的存在,线程仍然可以检测到这中间的变化。

public class AtomicStampedReferenceDemo {

    private static final Logger log = LoggerFactory.getLogger(AtomicStampedReferenceDemo.class);
    
    static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);

    public static void main(String[] args) throws InterruptedException {
        log.debug("main start...");
        // 获取值 A
        String prev = ref.getReference();
        // 获取版本号
        int stamp = ref.getStamp();
        log.debug("版本 {}", stamp);
        // 如果中间有其它线程干扰,发生了 ABA 现象
        other();
        TimeUnit.MILLISECONDS.sleep(1); // 使用TimeUnit使代码更具可读性
        // 尝试改为 C
        log.debug("change A->C {}", ref.compareAndSet(prev, "C", stamp, stamp + 1));
    }

    private static void other() {
        new Thread(() -> { // 更新如果成功,版本号加1
            log.debug("change A->B {}", ref.compareAndSet(ref.getReference(), "B",
                    ref.getStamp(), ref.getStamp() + 1));
            log.debug("更新版本为 {}", ref.getStamp());
        }, "t1").start();

        TimeUnit.MILLISECONDS.sleep(500); // 确保t1先启动
        new Thread(() -> {
            log.debug("change B->A {}", ref.compareAndSet(ref.getReference(), "A",
                    ref.getStamp(), ref.getStamp() + 1));
            log.debug("更新版本为 {}", ref.getStamp());
        }, "t2").start();
    }

    private static void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException(e);
        }
    }
}

AtomicMarkableReference(仅维护是否修改过)
AtomicStampedReference不同,它通过一个布尔标记(mark),来简单指示引用的对象是否曾被修改过。
这个类在执行CAS时,不仅关注引用本身的比较,还会检查这个伴随的标记状态。即,哪怕对象的值在一段时间内经历了A->B->A,由于标记的存在,线程也能够感知到该对象曾经发生过变化。
在这里插入图片描述

// GarbageBag类定义
class GarbageBag {
    private String desc;

    public GarbageBag(String desc) {
        this.desc = desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

    @Override
    public String toString() {
        return "GarbageBag{" +
                "desc='" + desc + '\'' +
                '}';
    }
}

public class TestABAAtomicMarkableReference {

    private static final Logger log = LoggerFactory.getLogger(TestABAAtomicMarkableReference.class);

    public static void main(String[] args) throws InterruptedException {
        GarbageBag bag = new GarbageBag("装满了垃圾");
        // 参数2 mark 可以看作一个标记,表示垃圾袋是否已满
        AtomicMarkableReference<GarbageBag> ref = new AtomicMarkableReference<>(bag, true);
        log.debug("主线程 start...");
        GarbageBag prev = ref.getReference();
        log.debug(prev.toString());

        new Thread(() -> {
            log.debug("打扫卫生的线程 start...");
            bag.setDesc("空垃圾袋"); // 假设这里清理了垃圾袋
            // 尝试将标记从true改为false,表示垃圾袋已清空
            while (!ref.compareAndSet(bag, bag, true, false)) {}
            log.debug(bag.toString());
        }).start();

        TimeUnit.SECONDS.sleep(1); // 等待打扫卫生的线程执行
        log.debug("主线程想换一只新垃圾袋?");
        boolean success = ref.compareAndSet(prev, new GarbageBag("空垃圾袋"), true, false);
        log.debug("换了么?" + success);
        log.debug(ref.getReference().toString());
    }
}

其他文章

【强烈推荐!】从底层源码剖析AQS的来龙去脉!(通俗易懂)

标签:一文,CAS,线程,本质区别,悲观,debug,prev,ref,log
From: https://blog.csdn.net/WLKQNYJY_SHT/article/details/140086735

相关文章

  • 通信协议 | 一文搞懂SPI通信协议
    SPI的英文全称为SerialPeripheralInterface,顾名思义为串行外设接口。SPI是一种同步串行通信接口规范,主要应用于嵌入式系统中的短距离通信。该接口由摩托罗拉在20世纪80年代中期开发,后发展成了行业规范。SPI是一种高速的、全双工的、同步的通信总线,并且至多仅需使用......
  • 一文速览计算机发展史
    起源:计算机的起源可以追溯到古代的计算工具,如算盘、机械计算器等。然而,真正意义上的现代计算机,其起源和发展历史却始于二十世纪中叶。在此之前,人们主要使用的是手动计算或简单的机械计算器来进行计算。1937年,美国数学家冯·诺依曼提出的“存储程序”的概念,这一想法奠定了......
  • python - [12] 脚本一文通
    题记部分 一、文件夹&文件(1)删除空文件夹#删除目录中的空文件夹importosdefmove_epty_folders(directory_path):forroot,dirs,filesinos.walk(directory_path,topdown=False):forfolderindirs:folder_path=os.path.join(root,f......
  • 一文搞懂到底什么是 AQS
    前言日常开发中,我们经常使用锁或者其他同步器来控制并发,那么它们的基础框架是什么呢?如何实现的同步功能呢?本文将详细讲解构建锁和同步器的基础框架--AQS,并根据源码分析其原理。一、什么是AQS?1.AQS简介AQS(AbstractQueuedSynchronizer),抽象队列同步器,它是用来构建锁或其他......
  • 一文带你入门机器学习分类算法
    专栏介绍1.专栏面向零基础或基础较差的机器学习入门的读者朋友,旨在利用实际代码案例和通俗化文字说明,使读者朋友快速上手机器学习及其相关知识体系。2.专栏内容上包括数据采集、数据读写、数据预处理、分类\回归\聚类算法、可视化等技术。3.需要强调的是,专栏仅介绍主流、......
  • 【AppStore】一文让你学会IOS应用上架Appstore
    前言咱们国内现在手机分为两类,Android手机与苹果手机,现在用的各类APP,为了手机的使用安全,避免下载到病毒软件,官方都极力推荐使用手机自带的应用商城进行下载,但是国内Android手机品类众多,手机商城各式各样,做不到统一,所以Android的APP上架得一个一个平台去申请上架,一直让开发人员头......
  • 一文带你看懂什么是营销归因模型及SaaS企业的应用
    在数字化时代,营销活动的多样性和复杂性使得评估其效果成为一项挑战。营销归因模型应运而生,为SaaS企业等提供了科学、系统的评估工具。本文将简要介绍什么是营销归因模型,阐述其带来的好处,并探讨SaaS企业可以采用的营销归因系统。什么是营销归因模型?营销归因模型是一种方法论,......
  • 一文为你深度解析LLaMA2模型架构
    本文分享自华为云社区《【云驻共创】昇思MindSpore技术公开课大咖深度解析LLaMA2模型架构》,作者:Freedom123。一、前言随着人工智能技术的不断发展,自然语言处理(NLP)领域也取得了巨大的进步。在这个领域中,LLaMA展示了令人瞩目的性能。今天我们就来学习LLaMA2模型,我们根据 昇思M......
  • 一文读懂HW护网行动(附零基础学习教程)
    前言随着《网络安全法》和《等级保护制度条例2.0》的颁布,国内企业的网络安全建设需与时俱进,要更加注重业务场景的安全性并合理部署网络安全硬件产品,严防死守“网络安全”底线。“HW行动”大幕开启,国联易安誓为政府、企事业单位网络安全护航!网络安全形势变得尤为复杂严峻。......
  • 【基础知识】497- 一文读懂Base64编码
    看到一篇特别好的文章:https://cloud.tencent.com/developer/article/1584718,感谢大佬分享。  一、为什么要使用base64我们知道一个字节可表示的范围是0~255(十六进制:0x00~0xFF),其中ASCII值的范围为0~127(十六进制:0x00~0x7F);而超过ASCII范围的128~255(十六进制:0x80~0......