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

news2024/7/4 22:32:30

文章目录

  • 悲观锁是什么?
  • 乐观锁是什么?
  • 如何实现乐观锁?
    • 什么是CAS
    • 应用
    • 局限性
    • ABA问题是什么?

悲观锁是什么?

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

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

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

乐观锁是什么?

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

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

如何实现乐观锁?

什么是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的来龙去脉!(通俗易懂)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1880217.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

primeflex overflow样式类相关的用法和案例

文档地址&#xff1a;https://primeflex.org/overflow 案例1 <script setup> import axios from "axios"; import {ref} from "vue";const message ref("frontend variable") axios.get(http://127.0.0.1:8001/).then(function (respon…

库存管理系统基于spingboot vue的前后端分离仓库库存管理系统java项目java课程设计java毕业设计

文章目录 库存管理系统一、项目演示二、项目介绍三、部分功能截图四、部分代码展示五、底部获取项目源码&#xff08;9.9&#xffe5;带走&#xff09; 库存管理系统 一、项目演示 库存管理系统 二、项目介绍 基于spingboot和vue前后端分离的库存管理系统 功能模块&#xff…

如何利用python画出AHP-SWOT的战略四边形(四象限图)

在企业或产业发展的相关论文分析中&#xff0c;常用到AHP-SWOT法进行定量分析&#xff0c;形成判断矩阵后&#xff0c;如何构造整洁的战略四边形是分析的最后一个环节&#xff0c;本文现将相关代码发布如下&#xff1a; import mpl_toolkits.axisartist as axisartist import …

chrome.storage.local.set 未生效

之前chrome.storage.local.set 和 get 一直不起作用 使用以下代码运行成功。 chrome.storage.local.set({ pageState: "main" }).then(() > {console.log("Value is set");});chrome.storage.local.get(["pageState"]).then((result) > …

java之命令执行审计思路

1 漏洞原理 因用户输入未过滤或净化不完全&#xff0c;导致Web应用程序接收用户输入&#xff0c;拼接到要执行的系统命令中执行。一旦攻击者可以在目标服务器中执行任意系统命令&#xff0c;就意味着服务器已被非法控制。 2 审计中常用函数 一旦攻击者可以在目标服务器中执行…

2024 Parallels Desktop for Mac 功能介绍

Parallels Desktop的简介 Parallels Desktop是一款由Parallels公司开发的桌面虚拟化软件&#xff0c;它允许用户在Mac上运行Windows和其他操作系统。通过强大的技术支持&#xff0c;用户无需重新启动电脑即可在Mac上运行Windows应用程序&#xff0c;实现了真正的无缝切换。 二…

基于LangChain+LLM的本地知识库问答:从企业单文档问答到批量文档问答

前言 过去半年&#xff0c;随着ChatGPT的火爆&#xff0c;直接带火了整个LLM这个方向&#xff0c;然LLM毕竟更多是基于过去的经验数据预训练而来&#xff0c;没法获取最新的知识&#xff0c;以及各企业私有的知识 为了获取最新的知识&#xff0c;ChatGPT plus版集成了bing搜索…

11_电子设计教程基础篇(磁性元件)

文章目录 前言一、电感1、原理2、种类1、制作工艺2、用途 3、参数1、测试条件2、电感量L3、品质因素Q4、直流电阻&#xff08;DCR&#xff09;5、额定电流6、谐振频率SRF&#xff08;Self Resonant Frequency&#xff09;7、磁芯损耗 4、应用与选型 二、共模电感1、原理2、参数…

第一周:李宏毅机器学习笔记

第一周学习周报 摘要一、机器学习基础理论1. 什么是机器学习&#xff1f;2. 机器学习“寻找”的函数有哪些类型&#xff1f;3. 机器学习中机器如何“寻找”函数&#xff1f;三步走3.1 第一步&#xff1a;设定函数的未知量&#xff08;Function with Unknown Parameters&#xf…

大多数博客首页都在使用的文字打字机出现效果

打字机效果展示 原理步骤 初步框架 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</…

vue使用axios获取信息的案例

List组件&#xff08;用来展示搜索的信息&#xff09; <template><div class"row"><!-- 列表数据 --><div class"card" v-for"user in info.users" :key"user.login" v-show"info.users.length">&l…

在 Windows 下使用 Linux 命令的多种方法

在 Windows 操作系统上使用 Linux 命令行工具&#xff0c;对于许多开发者和系统管理员来说是一个常见的需求。特别是对于那些习惯于 Linux 命令行的用户来说&#xff0c;Windows 自带的 CMD 和 PowerShell 可能并不满足他们的需求。虽然 Windows Subsystem for Linux (WSL) 是一…

【JavaEE】多线程代码案例(1)

&#x1f38f;&#x1f38f;&#x1f38f;个人主页&#x1f38f;&#x1f38f;&#x1f38f; &#x1f38f;&#x1f38f;&#x1f38f;JavaEE专栏&#x1f38f;&#x1f38f;&#x1f38f; &#x1f38f;&#x1f38f;&#x1f38f;上一篇文章&#xff1a;多线程&#xff08;2…

力扣 单词规律

所用数据结构 哈希表 核心方法 判断字符串pattern 和字符串s 是否存在一对一的映射关系&#xff0c;按照题意&#xff0c;双向连接的对应规律。 思路以及实现步骤 1.字符串s带有空格&#xff0c;因此需要转换成字符数组进行更方便的操作&#xff0c;将字符串s拆分成单词列表…

Linux --账号和权限管理

目录 1、 管理用户账号和组账概述 1.1 用户账号分类 1.2 组账号 1.3 UID 和 GID 2、用户账号文件 2.1 passwd 2.2 shadow 3、管理目录和文件属性 3.1 chage 命令 3.2 useradd 命令 3.3 passwd 命令 ​编辑3.4 usermod 命令 3.5 userdel 命令 4、用户账户的初始配置…

Spring企业开发核心框架-下

五、Spring AOP面向切面编程 1、场景设定和问题复现 ①准备AOP项目 项目名&#xff1a;Spring-aop-annotation ②声明接口 /*** - * / 运算的标准接口!*/ public interface Calculator { int add(int i, int j); int sub(int i, int j); int mul(int i, in…

使用Python绘制太阳系图

使用Python绘制太阳系图 太阳系图太阳系图的优点使用场景 效果代码 太阳系图 太阳系图&#xff08;Sunburst Chart&#xff09;是一种层次结构图表&#xff0c;用于表示数据的分层结构。它使用同心圆表示各个层级&#xff0c;中心圆代表最高层级&#xff0c;向外的圆环代表逐级…

类型转换与数据绑定【Spring源码学习】

simpleTypeConverter 类型转换 SimpleTypeConverter typeConverter new SimpleTypeConverter(); Integer number typeConverter.convertIfNecessary("13",int.class); System.out.println(number);BeanWrapper 通过反射原理为bean赋值&#xff0c;走的是set方法…

使用pyqt5编写一个七彩时钟

使用pyqt5编写一个七彩时钟 效果代码解析定义 RainbowClockWindow 类初始化用户界面显示时间方法 完整代码 在这篇博客中&#xff0c;我们将使用 PyQt5 创建一个简单的七彩数字时钟。 效果 代码解析 定义 RainbowClockWindow 类 class RainbowClockWindow(QMainWindow):def _…