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

浅谈乐观锁与悲观锁

乐观锁和悲观锁是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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【C语言】必备Linux命令和C语言基础

🌟博主主页:我是一只海绵派大星 📚专栏分类:嵌入式笔记 ❤️感谢大家点赞👍收藏⭐评论✍️ 目录 一、文件和目录相关命令 Linux 的文件系统结构 文件系统层次结构标准FHS pwd命令 ls 列目录内容 文件的权限 c…

[董晓算法]搜索相关题目及模板

前言: 本系列是学习了董晓老师所讲的知识点做的笔记 董晓算法的个人空间-董晓算法个人主页-哔哩哔哩视频 (bilibili.com) 动态规划系列(还没学完) 【董晓算法】动态规划之线性DP问题-CSDN博客 【董晓算法】动态规划之背包DP问题&#xff…

NSSCTF | [SWPUCTF 2021 新生赛]ez_unserialize

打开题目,发现是一个GIF动图页面,什么都没有。我们来查看一下源代码,看看有没有什么发现 其实只要足够了解网站的构成,看到绿色的注释字样就能知道它这里在提示robots.txt文件。 但是对于小白来说,也不知道它是什么&a…

高效稳定,AH1515-15V转12V8A稳压芯片成为各种设备首选

高效稳定,AH1515 15V转12V稳压芯片成为各种设备首选 随着科技的不断发展,各种电子设备对电源的要求越来越高,尤其是电压稳定性和电流输出能力。为了解决这一问题,我国一款名为AH1515的稳压芯片应运而生,凭借其优异的性…

win11家庭中文版安装docker,报错 Docker Engine stopped

先引一下这位博主的链接超详细Windows11家庭中文版系统安装Docker-20230401_windows11安装docker-CSDN博客,我到前五步(跳出页面重启)和博主都是一样的,但是第六步我并没有报错,直接跳出docker界面 记录一下我的解决办法,首先按照…

[链表专题]力扣141, 142

1. 力扣141 : 环形链表 题 : 给你一个链表的头节点 head ,判断链表中是否有环。 如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾…

2024042001-计算机网络 - 物理层

计算机网络 - 物理层 计算机网络 - 物理层 通信方式带通调制 通信方式 根据信息在传输线上的传送方向,分为以下三种通信方式: 单工通信:单向传输半双工通信:双向交替传输全双工通信:双向同时传输 带通调制 模拟信号…

深度盘点解析一家公司的PMO的定位与发展规划

对于一家公司的PMO(项目管理办公室)的定位与发展规划,今天从以下几个方面进行深度盘点和解析,具体如下: 一、PMO的定位 1. 战略与推手:PMO是持续提升组织项目管理水平,实现组织所有项目成功的重要推手。它站在公司战略的高度,从全局视角出发,对公司内部的项目进行统一…

使用高防IP是应对网络安全的重要措施

使用高防IP(High Defense IP)在现代网络环境中显得尤为重要,这主要源于以下几个方面的原因: 一、网络安全形势严峻 随着互联网的快速发展,网络安全问题日益突出。各种网络攻击手段层出不穷,如分布式拒绝服…

结合小波变换的遥感语义分割网络,融合频域和空间域特征提升分割效果

题目:SFFNet: A Wavelet-Based Spatial and Frequency Domain Fusion Network for Remote Sensing Segmentation 论文:http://arxiv.org/abs/2405.01992 代码:https://github.com/yysdck/SFFNet 年份:2024 创新点 两阶段网络SFFNet:网络首先使用空间方法提取特征,以保…

论:即时战略RTS游戏的小地图采用 自下而上的汇报式 还是 自上而下的查找式?

关键词:RTS 小地图 游戏设计 思路 卫星 位置映射 阵营 更新 汇报 询问 UE4 UE5 Unreal Engine 前言 你是否想过类似红色警戒的战略小地图的要素是采用何种方式更新数据的。大量数据实时更新,考虑频率,运行效率,开发中如何选型&a…

「每日跟读」英语常用句型公式 第15篇

「每日跟读」英语常用句型公式 第15篇 1. It’s only logical that __ 合理的做法/结论是__ It’s only logical that we should take a break (合理的做法是我们应该休息一下) It’s only logical that we work hard to make money(合理…

SQL注入漏洞常用绕过方法

SQL注入漏洞 漏洞描述 Web 程序代码中对于用户提交的参数未做过滤就直接放到 SQL 语句中执行,导致参数中的特殊字符打破了原有的SQL 语句逻辑,黑客可以利用该漏洞执行任意 SQL 语句,如查询数据、下载数据、写入webshell 、执行系统命令以及…

详细分析Vue3中的ref(附Demo)

目录 前言1. 基本知识2. Demo 前言 由于新项目涉及Vue3,本着探究问题的本质研究所不会的疑问 1. 基本知识 ref 是 Vue 3 中用于创建响应式数据的函数 接收一个初始值并返回一个包含了该值的响应式引用对象与 Vue 2.x 中的 data 属性不同,ref 返回的是…

【JAVA入门】Day05 - 面向对象

【JAVA入门】Day05 - 面向对象 文章目录 【JAVA入门】Day05 - 面向对象一、对象的设计和使用1.1 类和对象1.2 类的分类 二、封装三、private 关键字四、this 关键字五、构造方法六、JavaBean七、对象的内存图7.1 一个对象的内存图7.2 两个对象的内存图7.3 两个引用指向同一个对…

一种请求头引起的跨域问题记录(statusCode = 400/CORS)

问题表象 问题描述 当我们需要在接口的headers中添加一个自定义的变量的时候,前端的处理是直接在拦截器或者是接口配置的地方直接进行写,比如下面的这段比较基础的写法: $http({method: "post",url:constants.backend.SERVER_LOGIN…

函数栈帧的创建和销毁(详细理解)

🎁个人主页:我们的五年 🔍系列专栏:c语言课程学习 🎉欢迎大家点赞👍评论📝收藏⭐文章 目录 问题: 1.ebp,esp两个寄存器用来维护函数栈帧 2.main函数也一个函数&#…

QCustomPlot的了解

(一)QCustomPlot常见属性设置、多曲线绘制、动态曲线绘制、生成游标、矩形放大等功能实现-CSDN博客 关键代码: QT core gui printsupport 使用上面文章中的代码跑起来的程序效果图: 我的学习过程: 最开始初…

java 项目通用数据权限设计

文章目录 前言一、常见的数据权限二、通用数据权限设计思路通用权限示例(灵活配置最简单方式)两个表业务理解最终拼接出来的sql 为: 总结 前言 权限一般分为操作权限和数据权限 操作权限: 菜单,页面,按钮 数据权限: 能看到的数据,包括各种页面的数据范围 一、常见的数据权限 …

kafka用java收发消息

用java客户端代码来对kafka收发消息 具体代码如下 package com.cool.interesting.kafka;import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.apache.kafka.clients.consumer.ConsumerRecords; i…