【谈一谈】并发编程_锁的分类

【谈一谈】并发编程_锁的分类

Hello!~大家好!~每天进步一点点,日复一日,我们终将问剑顶峰

这里主要是介绍下我们常用的锁可以分为几类,目的是整体框架作用~方便后续的并发文章

说白了,这篇就是开头哈~

在这里插入图片描述

本文总纲:

在这里插入图片描述

一.可重入锁和不可重入锁

我们开发中一般用到的都是可重入锁比如Synchronized,ReentrantReadWriteLock,ReentrantLock都是可重入的

不可重入的锁很少见,也不怎么用到(如果要用,一般都自己通过Lock定义实现)

1.可重入锁:

它也称为递归锁,啥意思呢?

  • 是指同一线程在获取锁(如A锁)之后,可以再次对该锁(A)进行获取,而不会造成死锁。
  • 这种锁支持同一个线程对资源的重复加锁,并且在释放锁时,必须是获取锁的次数与释放锁的次数相等时,才会真正释放锁

还是有点迷糊吗?我们举个JavaReentrantLock实现可重入锁的简单例子来加深理解:

ReentrantLock锁机制:(我先说下,不然看下面代码,估计不怎么好理解哈)

  • 内部维护了一个计数器
  • 每当线程获取锁时,计数器加1;每当线程释放锁时,计数器减1
  • 只有当计数器为0时,其他线程才有机会获取该锁

下面代码:

  • doSomething()方法调用了doSomethingElse()方法,由于ReentrantLock可重入特性,第二次调用lock()不会导致线程阻塞,而是使锁计数器加1
  • 当对应的unlock()方法被调用两次后,锁才会真正释放允许其他线程获取锁
import java.util.concurrent.locks.ReentrantLock;public class ReentrantLockExample {private final ReentrantLock lock = new ReentrantLock();public void doSomething() {lock.lock(); // 获取锁try {// 在这里执行临界区代码doSomethingElse();} finally {lock.unlock(); // 无论是否发生异常,最终都要释放锁}}private void doSomethingElse() {lock.lock(); // 同一线程内再次获取锁,不会阻塞try {// 在这里执行另一段临界区代码} finally {lock.unlock(); // 释放锁}}
}

2.不可重入锁

是指一个线程获取锁后,在未释放该锁的情况下**,无法再次获取**该锁的同步机制,必须等此锁释放后,才能再获取锁

细心的同学可能会发现,在Java的标准库中,并没有直接提供不可重入锁的实现,为什么??

  • 因为在多层级调用或递归场景下(大多数的并发场景中),它们很容易造成意外死锁问题,
  • 而可重入锁(如ReentrantLock)则可以安全地支持这些复杂情况。

举个例子:(不考虑复杂场景哈~你别杠,我们只是演示下,目的是懂)

通过简单的计数器模拟,当锁被获取时,增加计数器,有且仅有计数器为0是才允许获取锁

下面的代码:

  • 这样的锁如果在线程内部递归调用lock方法,将会导致后续尝试获取锁的操作阻塞,从而表现出不可重入的特性。
public class NonReentrantLock {private boolean isLocked = false;private Thread holdingThread;public synchronized void lock() throws InterruptedException {while (isLocked && holdingThread != Thread.currentThread()) {wait();}isLocked = true;holdingThread = Thread.currentThread();}public synchronized void unlock() {if (holdingThread == Thread.currentThread()) {isLocked = false;holdingThread = null;notifyAll();} else {throw new IllegalMonitorStateException("当前线程并未持有此锁");}}
}

二.乐观锁和悲观锁

1.乐观锁 (Optimistic Locking)

  • 是一种在读取数据时不会立即加锁,而是在更新数据时才会检查在此期间是否有其他事务对数据进行了修改的并发控制策略
  • 假设大多数情况下不会有冲突发生(很乐观吧~哈哈哈,),因此在进行数据操作时保持乐观态度

在补充下:

数据库系统中,乐观锁通常通过版本号时间戳等机制实现

  • 当一个事务准备更新数据时,它会首先检查该数据的版本号时间戳是否与最初读取时一致
  • 如果一致: 则执行更新操作并更新版本号或时间戳;
  • 如果不一致,则表示在此期间有其他事务对该数据进行了修改,此时当前事务通常会选择回滚以避免覆盖其他事务的更改。

例子:

Hibernate中使用@Version注解:

  • 每次更新MyEntity实例时,Hibernate都会自动检查并更新version字段,从而实现了乐观锁的效果

又如Java中提供的CAS操作,典型的乐观锁实现

再举个实际点: 一个整数版本号来模拟乐观锁机制

transfer方法尝试进行转账操作时,

  1. 首先记录下当前账户的版本号。
  2. 然后模拟可能存在其他事务的情况,这里简单地直接增加版本号以示意图。
  3. 最后,在真正执行更新操作前,再次检查版本号是否与最初读取时一致。
  • 如果一致,则执行转账逻辑并递增版本号;
  • 如果不一致,则表示存在并发冲突,转账操作失败。

这样就实现了一个基于版本号的乐观锁机制,它可以防止在并发环境下的数据不一致性问题。

public class Account {private int balance; // 账户余额private int version; // 数据版本号public Account(int initialBalance) {this.balance = initialBalance;this.version = 0;}// 使用乐观锁进行转账操作public boolean transfer(Account to, int amount) {// 保存当前账户和目标账户的原始版本号int originalVersion = this.version;// 模拟并发环境下可能出现的其他事务操作simulateOtherTransactions();// 尝试更新账户余额和版本号if (this.version == originalVersion) {// 更新前检查版本号未变if (this.balance >= amount) {this.balance -= amount;to.balance += amount;// 成功更新数据后,将版本号递增this.version++;return true;} else {System.out.println("余额不足,转账失败");}} else {System.out.println("并发冲突,有其他事务修改了账户数据,转账失败");}return false;}// 模拟在转账操作过程中可能存在的其他事务对账户数据的修改private void simulateOtherTransactions() {// 这里仅用于演示,在实际应用中可能是由其他线程或事务引起的// 假设另一个事务在此时修改了账户数据并增加了版本号this.version++;}
}

2.悲观锁(Pessimistic Locking)

获取不到锁资源时,会将当前线程挂起(进入Blocked或者waitting)(有着一种生于忧患意识)

官方术语:

  • 是一种在访问数据时假设会发生并发冲突,并立即对数据进行加锁以防止其他事务或线程对其进行修改的并发控制策略。
  • 倾向于认为每次对数据的操作都可能引发并发问题,所以在获取数据前就先锁定资源

这种操作例子在数据库层面经常能看见:

如: 当第一个事务执行SELECT ... FOR UPDATE时,

  • 会对当前的查询记录进行锁定,此时其他任何事务试图读取或修改这条记录都会被阻塞,直到第一个事务提交或回滚释放锁

如: 在Java应用层面,

  • JDBC中的java.sql.Connection提供的setAutoCommit(false)方法:
  • 可以开启手动事务管理,配合数据库的悲观锁机制实现更细粒度的并发控制。

再举个: 以synchronized关键字为例,提供一个简单的线程安全的银行账户转账操作

  • transfer()方法通过synchronized关键字修饰,这意味着在同一时间只能有一个线程访问这个方法。

  • 当一个线程调用transfer进行转账操作时,其他线程必须等待当前线程完成操作并释放锁后才能继续执行。

    这就是悲观锁的应用,它假设并发环境下会存在数据冲突,并直接对资源进行锁定,以防止多个线程同时修改共享资源导致的数据不一致问题。

public class BankAccount {private double balance;public BankAccount(double initialBalance) {this.balance = initialBalance;}// 使用synchronized实现悲观锁public synchronized void transfer(BankAccount to, double amount) throws InterruptedException {if (this.balance >= amount) {// 锁定当前对象(即锁定该方法),确保同一时间只有一个线程能执行此方法Thread.sleep(100); // 模拟耗时操作,如数据库查询或更新this.balance -= amount;to.balance += amount;System.out.println("From: " + Thread.currentThread().getName() + ", Transfer " + amount + " completed.");} else {throw new IllegalArgumentException("Insufficient balance.");}}public static void main(String[] args) {BankAccount accountA = new BankAccount(100);BankAccount accountB = new BankAccount(0);Thread thread1 = new Thread(() -> accountA.transfer(accountB, 50));Thread thread2 = new Thread(() -> accountA.transfer(accountB, 60));thread1.start();thread2.start();try {thread1.join();thread2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Final balances: A=" + accountA.balance + ", B=" + accountB.balance);}
}

三.公平锁和非公平锁

这当中的关键点就是: 是否正常排队

1.公平锁

  • 是一种线程调度策略,它保证了等待锁的线程按照它们请求锁的顺序获得锁。
  • 公平锁机制下,当锁释放时,会优先分配给已经在队列中等待时间最长的线程,而不是随机选择一个等待的线程。

注意:

  • 在公平锁环境下,如果有多个线程在等待获取锁,那么锁会被分配给等待时间最久的那个线程,这种策略能够减少"线程饥饿"(即某些线程长时间无法获取到锁)的问题,提高系统的整体公平性
  • 公平锁虽然在理论上提供了更好的公平性,但可能会降低系统的整体吞吐量
    • 因为每次释放锁时都需要维护和检查等待队列,并且需要考虑线程上下文切换的成本。
    • 而在非公平锁(默认情况下)中,获取锁的线程可能是最近刚刚尝试获取锁的线程,这可能导致更高的并发性和系统性能,但可能也会导致某些线程长期得不到执行机会。

举个例子:

假设我们有一个共享资源(一个计数器)需要多个线程安全地进行递增操作:

ReentrantLock类:参数设为true,可以创建一个公平锁

在这个例子中:

我们创建了一个公平锁,并在一个共享的计数器上进行了递增操作

  • 当多个线程同时调用increment()方法时,公平锁会确保等待时间最长的线程优先获得锁并执行操作。

  • 由于每个线程在操作后都休眠了100毫秒,这有助于模拟实际的并发环境,使得不同线程之间的执行顺序更易于观察。

  • 公平锁策略下,理论上线程获取锁的顺序将尽可能按照它们请求锁的时间顺序进行,因此输出的结果应能体现出相对有序的执行过程。

import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.IntStream;public class FairLockExample {private final ReentrantLock lock = new ReentrantLock(true); // 创建一个公平锁private int counter = 0;public void increment() {lock.lock();try {counter++;System.out.println(Thread.currentThread().getName() + " incremented the counter to: " + counter);Thread.sleep(100); // 模拟耗时操作} catch (InterruptedException e) {Thread.currentThread().interrupt();} finally {lock.unlock();}}public static void main(String[] args) throws InterruptedException {FairLockExample example = new FairLockExample();IntStream.range(0, 10).forEach(i -> new Thread(() -> example.increment(), "Thread-" + i).start());Thread.sleep(5000); // 等待所有线程执行完成System.out.println("Final counter value: " + example.counter);}
}

2.非公平锁

是一种线程调度策略,与公平锁相反,

  • 在释放锁后并不保证等待时间最长的线程一定能获得锁
  • 当锁可用时,非公平锁可能会允许任何一个正在等待获取锁的线程获取锁,即使有其他线程已经等待了更长的时间。(适者生存,能者居之)

模拟场景说明:

线程A获取到锁资源,线程B没有拿到,线程B去排队,这时线程C跑来了,线程C咋么做呢?

  • 首先去尝试竞争一波
  • 竞争成功: 拿到锁,美滋滋进行执行
  • 竞争失败: 没有拿到锁资源,老老实实的排到B的后面,直到B拿到锁资源或者B取消后,才去竞争锁资源

举个例子:

使用非公平锁实现多线程安全递增操作的例子

在这个例子中

  1. 我们创建了一个非公平锁,并在一个共享的计数器上进行了递增操作
  2. 当多个线程同时调用increment()方法时,非公平锁可能让任何等待锁的线程获取到锁,而不考虑它们等待的先后顺序。
  3. 因此,输出的结果可能显示出线程获取锁和执行的相对无序性
  4. 虽然非公平锁可能导致某些线程“饥饿”(长时间无法获取锁),但在某些情况下,它能提供更高的吞吐量,因为减少了线程上下文切换的成本和队列维护的开销
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.IntStream;public class NonFairLockExample {private final ReentrantLock lock = new ReentrantLock(); // 创建一个非公平锁(默认false)private int counter = 0;public void increment() {lock.lock();try {counter++;System.out.println(Thread.currentThread().getName() + " incremented the counter to: " + counter);Thread.sleep(100); // 模拟耗时操作} catch (InterruptedException e) {Thread.currentThread().interrupt();} finally {lock.unlock();}}public static void main(String[] args) throws InterruptedException {NonFairLockExample example = new NonFairLockExample();IntStream.range(0, 10).forEach(i -> new Thread(() -> example.increment(), "Thread-" + i).start());Thread.sleep(5000); // 等待所有线程执行完成System.out.println("Final counter value: " + example.counter);}
}

完结撒花

在这里插入图片描述

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

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

相关文章

Photoshop 2023:重塑创意,引领数字艺术新纪元

在数字艺术的浩瀚星空中,Adobe Photoshop 2023(简称PS 2023)如同一颗璀璨的新星,为Mac和Windows用户带来了前所未有的创意体验。这款强大的图像处理软件不仅继承了前作的精髓,更在细节上进行了诸多创新,让每…

运行Python文件时出现‘utf-8’code can‘t decode byte 如何解决?(如图)

如图 亦或者出现“SyntaxError: Non-UTF-8 code starting with \xbb ” 出现这种问题往往是编码格式导致的,我们可以在py文件中的第一行加入以下代码: # codingutf-8或者 # codinggdk优先使用gbk编码 解释一下常用的两种编码格式: utf-…

朱维群将出席用碳不排碳碳中和顶层科技路线设计开发

演讲嘉宾:朱维群 演讲题目:“用碳不排碳”碳中和顶层科技路线设计开发 简介 姓名:朱维群 性别:男 出生日期:1961-09-09 职称:教授 1998年毕业于大连理工大学精细化工国家重点实验室精细化工专业&…

什么是B+树,和B树有什么不同?

👉博主介绍: 博主从事应用安全和大数据领域,有8年研发经验,5年面试官经验,Java技术专家,WEB架构师,阿里云专家博主,华为云云享专家,51CTO 专家博主 ⛪️ 个人社区&#x…

Spring Initializer环境问题

1.基于jdk8与本地 环境准备 1)下载jdk8并安装 2&#xff09;下载maven 3.6.3并解压放入D盘maven目录下&#xff0c;去掉外层 设置阿里源 打开settings.xml,在mirrors标签之内增加&#xff0c;注意粘贴后</id>中的/有可能被删掉&#xff0c;要自己补上 <mirror>&l…

健身房预约小程序制作详细步骤解析

如果你是一位健身爱好者&#xff0c;或者是一位健身教练&#xff0c;你一定知道预约健身的痛苦。传统的预约方式不仅麻烦&#xff0c;而且效率低下。但是&#xff0c;现在&#xff0c;我们可以使用一种神仙工具——乔拓云网&#xff0c;来搭建一个属于自己的健身预约小程序&…

【VTKExamples::PolyData】第四十三期 PolyDataPointSampler

很高兴在雪易的CSDN遇见你 VTK技术爱好者 QQ:870202403 前言 本文分享VTK样例PolyDataPointSampler,并解析接口vtkPolyDataPointSampler,希望对各位小伙伴有所帮助! 感谢各位小伙伴的点赞+关注,小易会继续努力分享,一起进步! 你的点赞就是我的动力(^U^)ノ~YO …

如何使用 CrewAI 构建协作型 AI Agents

一、前言 AI Agents 的开发是当前软件创新领域的热点。随着大语言模型 (LLM) 的不断进步&#xff0c;预计 AI 智能体与现有软件系统的融合将出现爆发式增长。借助 AI 智能体&#xff0c;我们可以通过一些简单的语音或手势命令&#xff0c;就能完成以往需要手动操作应用程序才能…

运维的利器–监控–zabbix–grafana

运维的利器–监控–zabbix–grafana 一、介绍 Grafana 是一个跨平台的开源的度量分析和可视化工具 , 可以通过将采集的数据查询然后可视化的展示 。zabbix可以作为数据源&#xff0c;为grafana提供数据&#xff0c;然后grafana将数据以图表或者其他形式展示出来。zabbix和gra…

基于YOLOv的目标追踪与无人机前端查看系统开发

一、背景与简介 随着无人机技术的快速发展&#xff0c;目标追踪成为无人机应用中的重要功能之一。YOLOv作为一种高效的目标检测算法&#xff0c;同样适用于目标追踪任务。通过集成YOLOv模型&#xff0c;我们可以构建一个无人机前端查看系统&#xff0c;实现实时目标追踪和可视化…

零基础学编程,中文编程工具之进度标尺构件的编程用法

零基础学编程&#xff0c;中文编程工具之进度标尺构件的编程用法 一、前言 今天给大家分享的中文编程开发语言工具 进度条构件的用法。 编程入门视频教程链接 https://edu.csdn.net/course/detail/39036 编程工具及实例源码文件下载可以点击最下方官网卡片——软件下载——…

机器人持续学习基准LIBERO系列9——数据集轨迹查看

0.前置 机器人持续学习基准LIBERO系列1——基本介绍与安装测试机器人持续学习基准LIBERO系列2——路径与基准基本信息机器人持续学习基准LIBERO系列3——相机画面可视化及单步移动更新机器人持续学习基准LIBERO系列4——robosuite最基本demo机器人持续学习基准LIBERO系列5——…

Python AI 实现绘画功能(附带源码)

本文我们将为大家介绍如何基于一些开源的库来搭建一套自己的 AI 作图工具。 需要使用的开源库为 Stable Diffusion web UI&#xff0c;它是基于 Gradio 库的 Stable Diffusion 浏览器界面 Stable Diffusion web UI GitHub 地址&#xff1a;GitHub - AUTOMATIC1111/stable-dif…

快速解决maven依赖冲突

我们在开发过程中经常出现maven依赖冲突&#xff0c;或者maven版本不匹配的情况&#xff0c;我们可以使用阿里云原生脚手架来做maven管理&#xff0c;添加需要的组件&#xff0c;然后点击获取代码&#xff0c;就可以获得对应的依赖文件。

【重要公告】对BSV警报系统AS的释义

​​发表时间&#xff1a;2024年2月15日 由BSV区块链协会开发并管理的BSV警报系统&#xff08;Alert System&#xff0c;以下简称“AS”&#xff09;是BSV网络的重要组件。它是一个复杂的系统&#xff0c;主要职能是在BSV区块链网络内发布信息。这些信息通常与网络访问规则NAR相…

C++基于多设计模式下的同步异步日志系统day4

&#x1f4df;作者主页&#xff1a;慢热的陕西人 &#x1f334;专栏链接&#xff1a;C基于多设计模式下的同步&异步日志系统 &#x1f4e3;欢迎各位大佬&#x1f44d;点赞&#x1f525;关注&#x1f693;收藏&#xff0c;&#x1f349;留言 只要内容主要实现了同步日志消息…

Kubernetes的Sevice管理

服务原理: 所有服务都是根据这个服务衍生或者变化出来,根服务---- 服务感知后端靠标签 slelector 标签选择器 kubectl label pods web1 appweb kubectl cluter-info dump | grep -i service-cluster-ip-range 服务ip取值范围 Service 管理: 创建服务: --- kind: Serv…

React富文本编辑器开发(六)

现在&#xff0c;相关的基础知识我们应该有个大概的了解了&#xff0c;但离我们真正的开发出一个实用型的组件还有一段距离&#xff0c;不过不用担心&#xff0c;我们离目标已经越来越近。 以现在我们所了解的内容而言&#xff0c;或许你发现了一个问题&#xff0c;就是我们的编…

CentOS配网报错:network is unreachable

常用命令&#xff1a; 打开&#xff1a; cd /etc/sysconfig/network-scripts/ 修改&#xff1a; vim ifcfg-ens33 打开修改&#xff1a; vim /etc/sysconfig/network-scripts/ifcfg-ens33 保存&#xff1a; 方法1&#xff1a;ESCZZ&#xff08;Z要大写&#xff09; 方…

Linux:地址空间的转换以及线程的理解和使用

文章目录 线程的理解地址空间的转换问题总结 线程的优点线程的缺点线程的健壮性问题 本篇主要进行对于进程和线程的理解&#xff0c;以及对于线程的一部分使用方法和使用的原理 线程的理解 首先回顾前面一篇的内容中&#xff0c;对于进程的基本认识&#xff1a; 什么是线程&…