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

2024-05-16 00:44
文章标签 通俗易懂 悲观 乐观

本文主要是介绍通俗易懂讲乐观锁与悲观锁,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

浅谈乐观锁与悲观锁

乐观锁和悲观锁是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.chinasem.cn/article/993390

相关文章

设计模式之工厂模式(通俗易懂--代码辅助理解【Java版】)

文章目录 1、工厂模式概述1)特点:2)主要角色:3)工作流程:4)优点5)缺点6)适用场景 2、简单工厂模式(静态工厂模式)1) 在简单工厂模式中,有三个主要角色:2) 简单工厂模式的优点包括:3) 简单工厂模式也有一些限制和考虑因素:4) 简单工厂模式适用场景:5) 简单工厂UML类图:6) 代码示例: 3、工厂方法模式1) 在工厂方法模式中,有4个主要角色:2) 工厂方法模式的工作流程

MySQL 如何实现乐观锁?

MySQL 如何实现乐观锁? 可以基于版本号实现乐观锁,修改数据的时候带上版本号(或者时间戳): UPDATE student SET name = '小李', version = 2 WHERE id = 100 AND version = 1 在每次数据更新的时候,都带上版本号,同时将版本 + 1,比如现在要更新 id = 1,版本号为 2 的记录,这时候就要先获取 id = 1的

[转载]最通俗易懂的p value讲解

什么是p value? 这个问题,曾一度让我怀疑我根本都没有学懂知识,只是像规则一样记住然后胡乱使用而已。此番记录就当再次考验我是否真正理解到p value的含义。 p value,代表在原假设条件下,实验事件可能发生的概率。举例说明:抛一枚硬币,正面朝上和反面朝上的概率是一样的,各50%,但这是有前提条件的,即硬币是均匀的(原假设),才能保证正反面出现的概率相同。现在将该硬币抛掷5次,那

面试官问:说说悲观锁、乐观锁、分布式锁?都在什么场景下使用?有什么技巧?...

点击上方“朱小厮的博客”,选择“设为星标” 后台回复"书",获取 后台回复“k8s”,可领取k8s资料 如何确保一个方法,或者一块代码在高并发情况下,同一时间只能被一个线程执行,单体应用可以使用并发处理相关的 API 进行控制,但单体应用架构演变为分布式微服务架构后,跨进程的实例部署,显然就没办法通过应用层锁的机制来控制并发了。 那么锁都有哪些类型,为什么要使用锁,锁的使用场景有哪些? 锁类别

Java 入门指南:Java 并发编程 —— Synchronized 实现悲观锁(Pessimistic Locking)

悲观锁 悲观锁(Pessimistic Locking)是一种悲观的并发控制机制,它基于悲观的假设,即并发冲突会时常发生,因此在访问共享资源(如数据库记录或共享变量)之前,会先获取独占性的锁,以防止其他线程对资源的并发读写。 悲观锁适用于写操作频繁、读操作较少的场景,能够确保数据一致性,但会引入较大的性能开销和线程切换的开销。 实现方式 在 Java 中,可以使用以下方式实现悲观锁: s

应用层协议(下)Https加密Http的秘密(含逻辑图解 简单易学 通俗易懂!)

绪论​ “如今我努力奔跑,不过是为了追上那个曾经被寄予厚望的自己 —— 约翰丶利文斯顿”,本章承接上章Http,没看过强烈建议看后再看本章,本章主要就是学习Https是干什么的并且去底层的学习Http的原理,将会讲到Https的加密、解密过程。 话不多说安全带系好,发车啦(建议电脑观看)。 Https协议 https也是一个应用层协议,是在http协议的基础上引入了一个加密层

人工智能核心概念最全解读!最通俗易懂的方式解读人工智能!

人工智能(AI)近年来成为了一个热词,但很多人对它的具体内容并不清楚。特别是当我们提到深度学习、机器学习、神经网络和自然语言处理时,很多人可能会感到一头雾水。 接下来,小编将会用通俗易懂的语言,带你逐一了解这些核心概念,并帮你理清它们之间的关系。无论你是刚刚接触AI的新人,还是对这个领域充满好奇的学习者,通过这篇文章,你都能对人工智能有一个更清晰、更全面的认识。 希望通过这次简明的介绍,你能发

MySQL 如何使用乐观锁和悲观锁

首先要说明的是:乐观锁和悲观锁并不是锁,而是锁的设计思想。 在 MySQL 中,乐观锁和悲观锁是两种用于解决数据库并发问题的机制。 悲观锁 悲观锁是指在访问数据时,认为数据会被其他事务修改,因此会采取锁定数据的方式来防止其他事务的修改。使用悲观锁时,一旦一个事务获取了锁,其他事务必须等待,直到第一个事务释放锁为止。 在 MySQL 中,悲观锁通常通过以下两种方式实现: SELECT … F

Mybatis Plus乐观锁实现

1 引言 乐观锁的主要作用在于提高系统的并发性能和减少锁冲突,同时保证数据的一致性。‌其原理简单来说就是,在修改数据的时候,判断数据是否被其他人改过,如果已被其他人改过,则修改失败。 2 代码 在SpringBoot 3.x+Mybatis Plus多数据源极简配置中的项目基础上更新代码,下面列出需要更新的代码。 2.1 org/example/mapper/InventoryMappe

【Hibernate】Hibernate对“悲观”和“乐观”锁的支持

首先,“锁”这个东西,可以认为是一种思想,悲观锁还是乐观锁,是人定义出来的一种概念,并非理解为DBMS的专属。换个称呼,叫做“悲观并发控制”或者“乐观并发控制”更便于我们理解二者的意义。          一、概念        ----(该部分内容来源于网络:http://www.open-open.com/lib/view/open1452046967245.html