通俗易懂讲乐观锁与悲观锁

news2024/12/25 23:58:48

浅谈乐观锁与悲观锁

乐观锁和悲观锁是Java并发编程中的两个概念。使用乐观锁和悲观锁可以解决并发编程中数据不一致性、死锁、性能差等问题,乐观锁与悲观锁的实行方式不同,所以其特性也不近相同,下文将详细介绍两者的特性与适用场景。

《熊出没》相信大家都了解过,接下来我将用《熊出没》中吉吉国王的视角来通俗易懂的讲述乐观锁与悲观锁。

悲观锁-总有刁民想害朕

吉吉国王在昨天摘了很多香蕉,在睡觉前没有吃完,于是它将剩下的香蕉存储起来留在第二天吃,由于吉吉国王身居高位,对于个人的饮食安全比较在意,因此它在扒出香蕉前总是在想:总有刁民想害朕,一定有其他猴子偷吃(数据减少、扣库存)本王的香蕉,或者给本王的香蕉下毒(修改数据),在吃之前我一定要好好检查一下。

悲观锁总是假设最坏的情况,即每次访问数据的时候,数据均被其他线程修改,所以悲观锁在每次使用时都会对所需资源进行上锁,如果其他线程获取该资源时会被阻塞,需要等待当前线程将资源释放。

Java悲观锁举例

synchronized关键字:synchronized关键字可以用来修饰方法或代码块,确保在同一时间只有一个线程可以访问被synchronized修饰的方法或代码块。当一个线程进入synchronized代码块时,会自动获取对象的锁,其他线程需要等待该线程释放锁才能访问。

public synchronized void synchronizedMethod() {
    // synchronized方法体
}

// 或者
public void someMethod() {
    synchronized(this) {
        // synchronized代码块
    }
}

ReentrantLock类:ReentrantLock是Java中的一种可重入锁,它提供了与synchronized类似的加锁和释放锁的功能,但相比synchronized更加灵活,可以支持公平锁和非公平锁,并且提供了更多的高级功能,如可中断锁、定时锁等。

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

public class Example {
    private Lock lock = new ReentrantLock();

    public void someMethod() {
        lock.lock(); // 获取锁
        try {
            // 锁保护的代码块
        } finally {
            lock.unlock(); // 释放锁
        }
    }
}

ReadWriteLock接口:ReadWriteLock接口提供了读写锁的功能,可以允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。ReentrantReadWriteLockReadWriteLock接口的默认实现。

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Example {
    private ReadWriteLock rwLock = new ReentrantReadWriteLock();

    public void readMethod() {
        rwLock.readLock().lock(); // 获取读锁
        try {
            // 读取共享资源
        } finally {
            rwLock.readLock().unlock(); // 释放读锁
        }
    }

    public void writeMethod() {
        rwLock.writeLock().lock(); // 获取写锁
        try {
            // 写入共享资源
        } finally {
            rwLock.writeLock().unlock(); // 释放写锁
        }
    }
}

悲观锁适用场景与缺陷

适用场景:悲观锁的核心观念是:每次方式资源时,该资源均被修改,因此悲观锁适用于写多、读少的业务场景

悲观锁缺陷:由于悲观锁每次使用时都需要对资源进行加锁,如果与其他线程存在资源竞争关系则可能会导致死锁互相阻塞的问题。

乐观锁-人之初,性本善

知识补充:

乐观锁版本机制: 一般是在数据表中加上一个数据版本号 version 或update_time字段,表示数据被修改的次数。

爱吃蜂蜜的熊大和熊二采集了一罐蜂蜜,它们约定每人每天吃一口(并发更新),吃完后在罐子上划上属于自己的线(线-版本),以证明自己吃过。熊二有时会贪吃,偶尔吃完一口还想再吃一口(数据被修改)。有一天,熊二不受控制地吃了两次,画了两道线。轮到熊大吃蜂蜜时,它发现罐子上的横线数量与上次吃蜂蜜时不一致(版本不一致),熊大意识到熊二又在嘴馋了,心里暗自嘀咕着这家伙真是没完没了。

出于熊大和熊二两兄弟间的信任,或者相信“天下还是好人多”,乐观锁总是相信共享资源没有被其他线程修改过,判断逻辑是通过版本机制或者CAS(compare and swap)算法实现。

版本机制

假设线程1要使用乐观锁对id为1的数据做修改,在修改前,需要先查询数据数据版本,然后再执行其他逻辑,在执行其他逻辑的期间,该数据可能被其他线程所修改,在下边的案例中修改了对应的数据,此时线程1并不知道其他线程修改了数据,为了判断数据是否被修改,线程1在更新时在where条件中校验数据版本,如果数据被修改过,则version版本不可能为1,因此,可以通过update语句的影响行数判断数据是否被修改。如果修改失败,则根据业务可使用重试机制。

create table orders
(
    id      int auto_increment
        primary key,
    price   decimal       null comment '金额',
    version int default 1 null comment '版本'
);


# 线程1查看数据版本
select version from orders where id = 1;

# 线程2修改了orders
update orders set price = 20.00, version = 2 where id = 1 and version = 1;

# 线程1做修改orders的操作
update orders set price = 30.00, version = 2 where id = 1 and version = 1;

使用Java代码模拟乐观锁的情况:

import java.util.concurrent.atomic.AtomicInteger;

class OptimisticLock {
    private AtomicInteger version = new AtomicInteger(0);
    private String data;

    public OptimisticLock(String data) {
        this.data = data;
    }

    // 读取数据
    public String readData() {
        return data;
    }

    // 更新数据
    public void updateData(String newData) {
        // 模拟在更新数据之前检查版本
        int oldVersion = version.get();
        // 模拟执行更新逻辑前,其他线程更新数据时,版本已经发生变化
        simulateConcurrency(); 
        if (oldVersion != version.get()) {
            System.out.println("Data update failed due to concurrent modification.");
            //可按业务需求来进行重试
            return;
        }
        data = newData;
        version.incrementAndGet();
        System.out.println("Data updated successfully. New version: " + version.get());
    }

    // 模拟并发访问,延迟一段时间
    private void simulateConcurrency() {
        try {
            Thread.sleep(1000); // 模拟并发情况下的延迟
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class Main {
    public static void main(String[] args) {
        OptimisticLock lock = new OptimisticLock("Initial data");

        // 线程1尝试更新数据
        new Thread(() -> {
            lock.updateData("Updated by Thread 1");
        }).start();

        // 线程2尝试更新数据
        new Thread(() -> {
            lock.updateData("Updated by Thread 2");
        }).start();
    }
}

在这个示例中,OptimisticLock 类代表一个具有乐观锁机制的数据对象。version 字段用于记录数据的版本号,每次更新数据时,版本号都会递增。在 updateData 方法中,首先检查旧版本和当前版本是否一致,如果一致则更新数据并递增版本号,否则认为更新失败。模拟了并发情况下的延迟和版本检查。

CAS算法

CAS:compoare and swap,比较和交换,CAS也可以理解为一种版本机制:比较期望值和待更新值是否一致,如果一致,则修改当前值为新值。CAS 是一个原子操作,底层依赖于一条 CPU 的原子指令。

CAS中的三个角色:

  • 待更新值:Var,简写V
  • 期望值:Expected
  • 新值:New(待写入值)

一只熊一天能吃一次蜂蜜,熊二贪嘴吃了两次蜂蜜,罐子上有两个杠,熊大期望熊二吃了一次,罐子上一个杠,轮到熊大吃蜂蜜时,熊大实际看到罐子上两个杠,与期望值不符,熊大没有吃蜂蜜,去告诉了妈妈(值不相等,不修改,抛出异常),第二天熊二知错就改,吃了一次蜂蜜,熊大看到与自己期望的一条杠一致,开心的吃了蜂蜜,画上了第二条杠(当前值与期望值一致,写入新值)。

CAS算法ABA问题

如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 "ABA"问题。

ABA 问题的解决思路是在变量前面追加上版本号或者时间戳。JDK 1.5 以后的 AtomicStampedReference 类就是用来解决 ABA 问题的,其中的 compareAndSet() 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

该段原地址:Java Guide ABA问题

悲观锁和乐观锁适用场景

悲观锁在使用时都会把公共资源进行加锁,其他线程处于阻塞状态,性能相较于乐观锁较低,综合以上,悲观锁适合写多、读少的业务场景

乐观锁在使用时会根据版本机制判断公共资源是否被修改过,如果被修改过会执行重试机制,如果写入频率较高,则会频繁进行重试,占用服务器CPU资源,综合以上,乐观锁适合读多、写少的场景

后续内容文章持续更新中…

近期发布。


关于我

👋🏻你好,我是Debug.c。微信公众号:种棵代码技术树 的维护者,一个跨专业自学Java,对技术保持热爱的bug猿,同样也是在某二线城市打拼四年余的Java Coder。

🏆在掘金、CSDN、公众号我将分享我最近学习的内容、踩过的坑以及自己对技术的理解。

📞如果您对我感兴趣,请联系我。

若有收获,就点个赞吧,喜欢原图请私信我。

wallhaven-gpkd77.jpg

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

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

相关文章

STM32-09-IWDG

文章目录 STM32 IWDG1. IWDG2. IWDG框图3. IWDG寄存器4. IWDG寄存器操作步骤5. IWDG溢出时间计算6. IWDG配置步骤7. 代码实现 STM32 IWDG 1. IWDG IWDG Independent watchdog,即独立看门狗,本质上是一个定时器,这个定时器有一个输出端&#…

ZYNQ之嵌入式驱动开发——字符设备驱动

文章目录 Linux驱动程序分类Linux应用程序和驱动程序的关系简单的测试驱动程序在petalinux中添加LED驱动新字符设备驱动 Linux驱动程序分类 驱动程序分为字符设备驱动、块设备驱动和网络设备驱动。 字符设备是按字节访问的设备,比如以一个字节收发数据的串口&#…

谷歌全力反击 OpenAI:Google I/O 2024 揭晓 AI 新篇章,一场激动人心的技术盛宴

🚀 谷歌全力反击 OpenAI:Google I/O 2024 揭晓 AI 新篇章,一场激动人心的技术盛宴! 在这个人工智能的全新时代,只有谷歌能让你眼前一亮!来自全球瞩目的 Google I/O 2024 开发者大会,谷歌用一场…

项目组GIT操作规范

分支规范 在开发过程中,一般会存在以下几种分支: main分支(master) master为主分支,也是用于部署生产环境的分支,一般由 dev 以及 fixbug分支合并,任何时间都不能直接修改代码。dev分支 develop 为开发分支&#xff…

Altium Designer封装库和元器件符号库下载与导入教程(SnapEDA 、Ultra Librarian、Alldatasheetcn)

1.AD封装库和元器件符号库下载网址 以下是一些全球热门的Altium Designer封装库和元器件符号库下载网址推荐: Altium Content Vault (现称为Altium Manufacturer Part Search):这是Altium官方提供的元器件库,可以直接在Altium Designer中使用…

Java码农的福音:再也不怕乱码了

即便是Java这样成熟的语言,开发者们也常常会遇到一个恼人的问题——乱码。 本文将深入探讨乱码的根本原因,并针对Java开发中的乱码场景提出有效的解决方案,辅以实战代码,让Java程序员从此告别乱码困扰。 一,字符集的…

文件存储解决方案-阿里云OSS

文章目录 1.菜单分级显示问题1.问题引出1.苹果灯,放到节能灯下面也就是id大于1272.查看菜单,并没有出现苹果灯3.放到灯具下面id42,就可以显示 2.问题分析和解决1.判断可能出现问题的位置2.找到递归返回树形菜单数据的位置3.这里出现问题的原因…

什么是最大路径?什么是极大路径?

最近学习中,在这两个概念上出现了混淆,导致了一些误解,在此厘清。 最大路径 在一个简单图G中,u、v之间的距离 d ( u , v ) min ⁡ { u 到 v 的最短路的长度 } d(u,v) \min \{ u到v的最短路的长度 \} d(u,v)min{u到v的最短路的…

音乐的力量

常听音乐的好处可以让人消除工作紧张、减轻生活压力、避免各类慢性疾病等等,其实这些都是有医学根据的。‍ 在医学研究中发现,经常的接触音乐节 奏、旋律会对人体的脑波、心跳、肠胃蠕动、神经感应等等,产生某些作用,进而促进身心…

Postman基础功能-接口返回值获取

大家好,之前给大家分享关于Postman的接口关联,我们平时在做接口测试时,请求接口返回的数据都是很复杂的 JSON 数据,有着多层嵌套,这样的数据层级在 Postman 中要怎么获取呢? 接下来给大家展示几个获取 JSO…

容联云零代码平台容犀desk:重新定义坐席工作台

在数智化浪潮的推动下,企业亟待灵活适应市场变化、快速响应客户需求,同时还要控制成本并提升效率,传统的软件开发模式因开发周期长、成本高、更新迭代慢等问题,逐渐难以满足企业灵活多变的业务需求。 容犀Desk,观察到…

(1)双指针算法介绍与练习:移动零

目录 双指针算法介绍 练习:移动零 双指针算法介绍 双指针算法常见于数组和双向链表的题型 在数组中,双指针中的指针代表数组元素的下标,而不是真正的指针类型变量 在双向链表中,双指针中的指针即为真正意义上的指针&#xff…

Windows安装Django

1、下载Python程序包 Python程序包官网下载地址Download Python | Python.org,若下载最新版本,有最新版本则下载"Windows installer (64-bit)" 若是下载其他版本,可在下图位置找到相应的版本,然后点击Download.如下图所示: 打开后查看注意事项…

开源连锁收银系统哪个好

针对开源连锁收银系统的选择,商淘云是一个备受关注的候选。商淘云以其功能丰富、易于定制和稳定性等优势,吸引了众多企业和开发者的关注。下面将从四个方面探讨商淘云开源连锁收银系统的优势: 首先,商淘云提供了丰富的功能模块。作…

Retrying,一个神奇优雅的 Python 库

大家好!我是爱摸鱼的小鸿,关注我,收看每期的编程干货。 一个简单的库,也许能够开启我们的智慧之门, 一个普通的方法,也许能在危急时刻挽救我们于水深火热, 一个新颖的思维方式,也许能…

Docker安装Mosquitto

在物联网项目中,我们经常用到MQTT协议,用MQTT协议做交互就需要部署一个MQTT服务,而mosquitto是一个常用的MQTT应用服务, Mosquitto是一个实现了消息推送协议MQTT v3.1的开源消息代理软件。MQTT(Message Queuing Teleme…

AI大模型日报#0515:Google I/O大会、 Ilya官宣离职、腾讯混元文生图大模型开源

导读:欢迎阅读《AI大模型日报》,内容基于Python爬虫和LLM自动生成。目前采用“文心一言”(ERNIE 4.0)、“零一万物”(Yi-34B)生成了今日要点以及每条资讯的摘要。 《AI大模型日报》今日要点:谷歌…

Java 自然排序和比较器排序区别?Comparable接口和Comparator比较器区别?

注:如果你对排序不理解,请您耐心看完,你一定会明白的。文章通俗易懂。建议用idea运行一下案例。 1)自然排序和比较器排序的区别? 自然排序是对象本身定义的排序规则,由对象实现 Comparable 接口&#xff…

什么?你设计接口什么都不考虑?

如果让你设计一个接口,你会考虑哪些问题? 1.接口参数校验 接口的入参和返回值都需要进行校验。 入参是否不能为空,入参的长度限制是多少,入参的格式限制,如邮箱格式限制 返回值是否为空,如果为空的时候是…

代码随想录算法训练营第二十九天 | 39. 组合总和、40.组合总和II、131.分割回文串

39. 组合总和 题目链接/文章讲解: 代码随想录 视频讲解:带你学透回溯算法-组合总和(对应「leetcode」力扣题目:39.组合总和)| 回溯法精讲!_哔哩哔哩_bilibili 解题思路 这里和组合不同的是元素可以重复选取…