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

浅谈乐观锁与悲观锁

乐观锁和悲观锁是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_incrementprimary 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.hqwc.cn/news/705097.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MinIO学习笔记

MinIO学习笔记 MinIO简介Springboot整合MinioMinIO中的Bucket、ObjectMinioClient的常用API:操作bucketMinioClient的常用API:操作对象 MinIO集群部署纠删码模式部署单机多磁盘的纠删码模式部署分布式集群部署 Nginx 视频学习地址 MinIO简介 MINIO干什…

机器人非线性系统反馈线性化与解耦

机器人非线性系统的反馈线性化和解耦是控制理论中的两个重要概念,它们分别用于简化系统分析和设计过程,提高控制系统的性能。 首先,反馈线性化是一种将非线性系统转化为线性系统的技术。在机器人控制中,由于机器人本身是一个强耦…

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

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

20231911 2023-2024-2 《网络攻防实践》实践九报告

1.实践内容 1.1 缓冲区 缓冲区是内存空间的一部分,在内存中预留了一定的存储空间,用来暂时保存输入和输出等I/O操作的一些数据,这些预留的空间就叫做缓冲区。 1.2 shellcode shellcode是一段用于利用软件漏洞而执行的代码,也可以…

第十四届蓝桥杯大赛软件赛国赛C/C++ 大学 B 组 数三角

//枚举顶点。 //不存在等边三角形 #include<bits/stdc.h> using namespace std; #define int long long const int n2e311; int a,b,c,l[n],r[n]; signed main() {ios::sync_with_stdio(false);cin.tie(0),cout.tie(0);cin>>a;for(int i1;i<a;i){cin>>…

【初阶数据结构】带头双向循环链表讲解

前言 &#x1f4da;作者简介&#xff1a;爱编程的小马&#xff0c;正在学习C/C&#xff0c;Linux及MySQL。 &#x1f4da;本文收录与初阶数据结构系列&#xff0c;本专栏主要是针对时间、空间复杂度&#xff0c;顺序表和链表、栈和队列、二叉树以及各类排序算法&#xff0c;持…

开源连锁收银系统哪个好

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

报错:(idea端口被占用)Web server failed to start. Port 9090 was already in use.

cmd里面输入&#xff1a; netstat -ano|findstr "9090" 可以看到pid是9644 然后再打开任务管理器

卷轴分红商城模式:适用于多种的商业营销模式

卷轴分红商城模式是一种基于区块链技术的去中心化积分商城系统&#xff0c;通过智能合约和数字资产分红实现积分流通和价值回馈&#xff0c;适用于多种场景。 什么是卷轴分红商城模式&#xff1a; 这是一个去中心化的积分商城系统&#xff0c;消费者在商城消费时&#xff0c;可…

GDPU 竞赛技能实践 天码行空 期末小测

1. 除法&#xff08;原题&#xff09; &#x1f468;‍&#x1f3eb; 实验二&#xff1a;1.简单枚举 输入正整数n&#xff0c;按从小到大的顺序输出所有形如abcde/fghij n的表达式&#xff0c;其中a&#xff5e;j恰好为数字0&#xff5e;9的一个排列&#xff08;可以有前导0&a…

终于搞懂Linux 设备树中的#address-cells,#size-cells 和reg 属性

目录 一、前置知识 1. 处理器平台2. reg 属性的基本格式3. reg 属性的作用 reg 用法 二、#address-cells 和 #size-cells 属性 1. 示例1 2. 示例23. 示例3 一、前置知识 要理解#address-cells和#size-cell 这两个属性&#xff0c;就要先了解 reg属性。 1. 处理器平台 下…

上班族兼职新篇章:10大实战攻略,轻松年赚1-20万

对于众多上班族而言&#xff0c;如何在工作之余赚取额外收入&#xff0c;开启自己的第一份副业&#xff0c;已成为许多人心中的疑问。每个人的才能和兴趣点不尽相同&#xff0c;但都有机会找到适合自己的兼职方式。接下来&#xff0c;就让我们一起探索这10大实战攻略&#xff0…