【多线程】锁策略

news2024/12/23 16:08:49

1. 说在前面

这里的锁策略内容,属于典型的面试八股文!如果未来工作,需要实现一把锁,那么得好好研究下锁策略,但基本上不会让我们自己设计一把锁的。

而这里的锁策略内容不局限于 Java,任何 "锁" 相关的话题都是可以应用本节锁策略的!

对于咱们来说,了解下锁策略也不是坏事,对于合理使用锁也是有点帮助的。


2. 常见的锁策略

2.1 乐观锁和悲观锁

  • 乐观锁:预测锁竞争不是很激烈(做的工作相对更少)

  • 悲观锁:预测锁竞争特别的激烈(做的工作相对更多)

如何理解乐观锁和悲观锁?

乐观锁又乐观在哪呢?乐观锁会假设数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式的对数据是否产生冲突而发生检测,如果并发冲突了,返回用户错误的信息,让用户决定如何去做。

悲观锁又悲观在哪呢?悲观锁每次都会假设是最坏的情况,所以每次去拿数据的时候都认为别人会修改,所以每次拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞等待!

上述干巴巴的叙述感觉还是很抽象,这里我们举一个现实中的例子吧:

我每天很忙,假设这时候有个妹子想找我陪她去逛街,那么妹子如何跟我表达她内心的想法呢?妹子有两种方案:

● 妹子并不觉得我很忙,觉得我肯定有时间能陪她逛街,所以妹子就直接来找我了(没有加锁,直接访问资源),如果我此时确实闲着了,那么就能直接陪这个妹子逛街了,如果我现在很忙,妹子来了后看见我在忙,就不会打扰我了,就会过一会再来找我(虽然没加锁, 但是能识别出数据访问冲突) 这就是乐观锁。

● 妹子是一个谨慎的妹子,总觉得我天天很忙,可能没时间陪她逛街,于是在找我之前,就给我发个消息:"你现在有时间没我逛街吗?"(相当于尝试加锁),如果我现在闲着,肯定会回复她有时间,于是等得到我的答复后,她才会过来找我逛街,如果我很忙,那就自然拒绝她了,那她就会等一段时间再来发消息问问我有没有时间陪她逛街,这就是悲观锁。

对于乐观锁和悲观锁,谁好谁坏具体看场景需求,具体在后面介绍 synchronized 章节介绍。

2.2 轻量级锁和重量级锁

  • 轻量级锁:加锁解锁开销比较小,效率更高,多数情况下,乐观锁也是一种轻量级锁

  • 重量级锁:加锁解锁开销比较大,效率更低,多数情况下,悲观锁也是一种重量级锁

在了解轻量级锁和重量级锁之前,我们需要理解为什么锁能保证原子性?

锁能保证原子性,追根溯源是 CPU 这样的硬件设备提供的机制。

首先 CPU 提供了 "原子操作指令",操作系统基于 CPU 的原子指令,实现了 mutex 互斥锁,JVM 基于操作系统提供的 mutex 互斥锁,实现了 synchronized 和 ReentrantLock 等关键字和类。

那么问题来了,轻量级锁相较于重量级锁,为什么轻在加锁解锁上?

轻量级锁加锁机制尽可能不使用 mutex,也就是不适用操作系统提供的互斥锁,而是尽量在用户态代码完成加锁解锁操作,实在搞不定,再使用 mutex,这样一来,就会进行少量的内核态和用户态切换,不太容易引发线程调度。

重量级锁加锁机制重度依赖了操作系统提供的 mutex,这样会大量引起内核态用户态切换,也很容易引发线程的调度,前面我们讲解线程池的时候,也了解过用户态是可控的,所以重量级锁的成本会更高,涉及到交给内核办事,就意味着,你不知道内核身上背负着多少的任务,效率自然也就不明确了 。

2.3 自旋锁和挂起等待锁

自旋锁是一种典型的轻量级锁,挂起等待锁是一种典型的重量级锁。

如何理解这两把锁呢,此时举一个形象的例子来方便大家理解:

有一个痴情小伙,准备跟心系已久的妹子表白(想对妹子加锁),没想到被妹子发了好人卡,原因居然是妹子已经有男朋友了(被其他人加锁了)。

这个小伙子虽然被妹子拒绝了,但是小伙很坚强!心里不甘!于是就想着妹子分手后他去上位!

此时妹子还在谈恋爱呢,小伙子是干等着吗?不一定!小伙子此时就有两种做法:

  1. 每天都跟妹子聊天,有事没事给妹子点外卖请喝奶茶,这样一来,一旦妹子分手了,由于小伙子天天与妹子聊天,所以小伙是能第一时间知道妹子分手的,也就是一旦锁被释放,就能第一时间感知到,从而有机会获取到锁,很明显这样很舔,也就相当于随时都在被 CPU 调用,随时都在判断锁有没有被释放,显然占用了大量的系统资源,这就是自旋锁。

  1. 小伙子被妹子拒绝了后,就默默在心里爱着她,默默的等着她,也不打扰妹子的恋爱生活,只是想着有一天妹子分手了,再去追求,过了一段时间,妹子分手了(锁被释放),可能会想起来这个小伙子,这时小伙子才有可能获取到锁,但是由于小伙子默默无闻,所以妹子大概率已经把小伙子给忘了,等想起来小伙子的时候,可能又谈了好几个男朋友了,这样的小伙子没有无时无刻在关注妹子是否分手,就相当于减少了对 CPU 的占用率,减少的时间 CPU 能去干其他事,但是啥时候能获取到锁,大概率是没有自旋锁来的快,这就是挂起等待锁!

2.4 互斥锁和读写锁

互斥锁就是前面学习过的 synchronized 这样的锁,提供加锁和解锁,如果一个线程对 A 加锁了,另一个线程也尝试对 A 加锁,此时就会阻塞等待。

读写锁见名知意,执行加锁的时候,需要额外表明读写意图,一个线程对于数据的访问,主要是存在两个操作:读数据和写数据。

  • 两个线程同时读一个数据,并没有线程安全问题!

  • 两个线程都去写一个数据,是有线程安全问题的!

  • 一个线程读,一个线程写同一个数据,是有线程安全问题的!

读写锁就是把读操作和写操作区别对待了,Java 标准库提供了 ReentrantReadWriteLock 类,实现了读写锁。

通过 ReentrantReadWriteLock 对象里的方法,可以获取读锁和写锁!

针对读加锁:ReentrantReadWriteLock.ReadLock,这个类表示了一个读锁,提供了 lock 和 unlock 方法进行加锁和解锁。

针对写加锁:ReentrantReadWriteLock.WriteLock,这个类表示了一个写锁,提供了 lock 和 unlock 方法进行加锁和解锁。

这里我们用两个线程尝试都就加读锁,看看会不会有互斥效果:

public static void main(String[] args) {
    ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    // readLock 是一个读锁, 提供 lock() 加锁, unlock() 解锁
    // reentrantReadWriteLock.readLock(); 获取读锁
    ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    Thread t1 = new Thread(() -> {
        readLock.lock();
        System.out.println("t1 针对读加锁了");
        try {
            Thread.sleep(10_000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        readLock.unlock();
    });
    Thread t2 = new Thread(() -> {
        readLock.lock();
        System.out.println("t2 针对读加锁了");
        readLock.lock();
    });
    t1.start();
    t2.start();
}

上述代码,一执行 t1 线程就会对 reentrantReadWriteLock 对象加读锁,然后打印,接着休眠 10s,再解锁,此时如果发生锁互斥,t2 就得等 t1.unlock() 之后才能执行 readLock.lock() 加锁操作,才能接着往后执行。

但是实际上 t2 并不用等 t1 解锁后才能加锁,因为 t1 和 t2 都是加的读锁,不会产生互斥!所以 t1 和 t2 是并发执行的!并不会受到加锁的影响,所以执行代码会发现 t2 并不会由于没有竞争到锁而阻塞等待,而是与 t1 并发执行!

那么一个线程加读锁,一个线程加写锁,是否会产生互斥呢?

public static void main(String[] args) {
    ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    // readLock 是一个读锁, 提供 lock() 加锁, unlock() 解锁
    ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    // writeLock 是一个读锁, 提供 lock() 加锁, unlock() 解锁
    ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
    Thread t1 = new Thread(() -> {
        readLock.lock();
        System.out.println("t1 针对读加锁了");
        try {
            Thread.sleep(10_000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("t1 解锁了!");
        readLock.unlock();
    });
    Thread t2 = new Thread(() -> {
        writeLock.lock();
        System.out.println("t2 针对写加锁了");
        writeLock.lock();
    });
    t1.start();
    t2.start();
}

这段代码就是 t1 线程对 reentrantReadWriteLock 加读锁,t2 线程对 reentrantReadWriteLock 加写锁,执行上述代码,t1 已经加读锁了,此时 t2 想加上写锁就得等!等 t1 执行到 readLock.unlock() 把这个 reentrantReadWriteLock 的读锁给释放了(解锁),此时 t2 才能加锁!

所以执行上述程序,就会发现,t1 执行完了,t2 才能加上写锁并往后执行代码。所以加写锁和读锁之间,存在互斥!

解答疑问:

可能看到这,有些小伙伴还有点懵,什么读锁,写锁的,这里我们一定要弄清楚,这里的读锁和写锁是从哪来的!

是通过这个类: ReentrantReadWriteLock ,实例化的对象,调用对应方法,来获取一个能加锁解锁的对象的,如果调用 readLock() 就能得到读锁,如果调用 writeLock() 就会得到写锁,所以我们上述说加读锁,加写锁,本质都是对 ReentrantReadWriteLock 实例出来的对象进行加读锁或写锁!

那么 t1 加写锁,t2 加写锁,是否也会互斥呢?也是会的!具体大家可以下来自行实验下!

总结:
读锁和读锁之间没有互斥!
写锁和读锁之间存在互斥!
写锁和写锁之间存在互斥!

2.5 公平锁和非公平锁

公平不公平?如何理解?这是一个值得思考的问题!

这里就不讨论哲学问题了,直接来学习什么是公平锁,什么是非公平锁。

假设有好多人在追同一个妹子!但是这个妹子在谈恋爱,所以追这个妹子的人只能等着!

此时妹子分手了,谁上位呢?公平锁就是谁先来的,也就是谁最先阻塞等待,谁就能在锁释放后(妹子分手),第一时间获取锁,后面的则继续阻塞等待!操作系统内部线程的调用,是随机的,想实现公平锁,就需要依赖额外的数据结构,记录线程的先后顺序了。上述情况就是公平锁!

什么是非公平锁?

这种情况就是非公平锁,一旦某个线程释放了锁,此时等待该锁的线程都会蜂拥而上,也就会出现锁竞争,那么谁能获取到锁呢?此时就是看 CPU 随机调度了,调度到谁,那就是谁获取到锁!

2.6 可重入锁和不可重入锁

这个我们前面讲死锁的时候讲到过,如果一个线程对同一个锁对象加锁两次,出现了死锁,那么就是不可重入锁。而 synchronized 是一把可重入锁!

那么这里我们就直接说结论了:

  • 如果一个线程对同一个对象加锁两次,出现死锁了,这就是不可重入锁

  • 如果一个线程对同一个对象加锁多次,都不会出现死锁,这就是可重入锁


下期预告:【多线程】CAS原理

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

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

相关文章

python接口自动化(十七)--Json 数据处理---一次爬坑记(详解)

简介 有些 post 的请求参数是 json 格式的,这个前面发送post 请求里面提到过,需要导入 json模块处理。现在企业公司一般常见的接口因为json数据容易处理,所以绝大多数返回数据也是 json 格式的,我们在做判断时候,往往只…

golang 实现四层负载均衡

大家好,我是蓝胖子,做开发的同学应该经常听到过负载均衡的概念,今天我们就来实现一个乞丐版的四层负载均衡,并用它对mysql进行负载均衡测试,通过本篇你可以了解到零拷贝的应用,四层负载均衡的本质以及实践。…

代码模版-element plus如何进行前端校验输入框

文章目录 步骤一:引入 element plus 框架步骤二:使用 element plus 的 form步骤三:form 明确指定 rules步骤四:事件触发校验 使用 vue3 element plus 步骤一:引入 element plus 框架 先 npm 安装 在 src/main.js 中…

linux[armbian]环境安装nginx

文章目录 linux[armbian]环境安装常用命令遇到的问题和解决方法问题一:conf/koi-win复制错误解决方法问题二:缺少相关的日志目录解决方法 linux[armbian]环境安装 下载Nginx: 访问[Nginx官方网站](https://nginx.org/)&#xff0c…

排序算法(1):冒泡排序

在计算机科学领域,排序算法是一个重要的主题。冒泡排序法是最基础且简单的排序算法之一,它的原理简单易懂,是学习排序算法的理想起点。本文将详细介绍冒泡排序法的原理、实现方法以及优化技巧,帮助读者全面了解和掌握这一经典算法…

重新安装conda时报错

自己因为最近利用 conda 安装了比较多的软件,在输入创建环境时: conda create -n python27 python2.7 报错,环境创建不成功。 显示 miniconda3 文件夹已存在 (因为安装时会默认安装至此目录,如果此目录已经存在则会冲突报错&…

【基础算法】贪心算法

贪心算法又称贪婪算法,是一种常见的算法思想。贪心算法的优点是效率高,实现较为简单,缺点是可能得不到最优解。 贪心算法的基本思想 贪心算法就是在求解问题时,总是做出当前看来最好的选择。也就是说贪心算法并不从整体最优上考…

word转PDF后图片为何会变小?怎么解决?

有些同学反映将Word文档转换为PDF后,发现里面的图片居然变小了,这是什么原因造成的?该怎么解决呢? 先来说说原因,我个人认为可能是由以下原因造成的: 1、word插入图片后压缩“太狠”了。当你在word中插入…

GENMARK控制器维修S08S4P.D工业电脑维修

机器人GENMARK SYSTEM CONTROLLER系统控制器维修S08S4P.D工业电脑;晶圆转移机器人SΛΛALL CONTROLLER; SΛΛC1100 半导体设备机械臂GENMARK控制器等 GenMark的两大构架:eSensor(电子传感)和Elecitrowetting&#xf…

ibaq intensity 蛋白组学 蛋白质组学两个定量方法(iBAQ和LFQ)的区别及常见的标准化方法

4.MaxQuant中的Intensity,LFQ和iBAQ 大佬的软件,三种定量算法都发了文章。 Intensity是将某Protein Groups里面的所有Unique和Razor peptides的信号强度加起来,作为一个原始强度值。用得很少。iBAQ是在Intenstiy的基础上,将原始…

JUC#线程池加锁逻辑梳理

带着问题看源码 为什么要用线程池?Java是实现和管理线程池有哪些方式? 请简单举例如何使用。为什么很多公司不允许使用Executors去创建线程池? 那么推荐怎么使用呢?ThreadPoolExecutor有哪些核心的配置参数? 请简要说明ThreadPoolExecutor可以创建的是哪三种线程池呢?当…

【Web3】Web3Js高频Api

目录 Web3Js方法 初始化Web3实例 Web3Api 创建账号Api 获取余额Api 单位转换工具函数 Web3Js方法 web3.eth:用于与以太坊区块链和智能合约之间的交互。 web3.utils:包含一些辅助方法。 web3.shh:用于协议进行通信的P2P和广播。 web3…

1066 Root of AVL Tree (PAT甲级)

这道题类似1123题。 #include <cstdio> #include <algorithm>struct node{int key;node* left nullptr;node* right nullptr; };int N, t; node* root nullptr;int getHeight(node* r){if(!r){return 0;}return std::max(getHeight(r->left), getHeight(r-&…

【json-server】json-server安装与使用:

文章目录 一、下载安装:二、启动db.json数据及相关参数&#xff1a;三、创建json数据——db.json&#xff1a;四、修改端口号&#xff1a;五、操作数据&#xff1a;【1】常规获取&#xff1a;【2】过滤获取 Filter:【3】分页 Paginate&#xff1a;【4】排序 Sort&#xff1a;【…

使用 .editorconfig 文件来统一编程风格

做过长期开发的程序员都知道保持编程风格统一的重要性, 统一的风格能够降低各种成本. 有一句名言是咋说的来着? 代码主要是给人看的, 其次才是给电脑去运行. 但另一方面, 大家又普遍是偷懒的, 对于这些长期会受益, 但短期收益不明显甚至带来麻烦的事, 许多团队中的成员不能说抵…

产品经理进阶:硬件产品定价指南

目录 介绍 基于成本的定价 基于市场的定价 基于价值的定价 总结一下 CSDN学员课程 优惠活动通知 介绍 定价本身其实是一个相对复杂的过程。 因为有很多变量会影响到你最终的定价。 比如说&#xff1a;客户的维度、竞争对手的维度、成本的维度等等。 但是无论如何&am…

Impala3.4源码阅读笔记(三)data-cache的Store实现

前言 本文为笔者个人阅读Apache Impala源码时的笔记&#xff0c;仅代表我个人对代码的理解&#xff0c;个人水平有限&#xff0c;文章可能存在理解错误、遗漏或者过时之处。如果有任何错误或者有更好的见解&#xff0c;欢迎指正。 正文 本文顺承前文Impala3.4源码阅读笔记&a…

mac电脑上,webm格式怎么转换成mp4?

mac电脑上&#xff0c;webm格式怎么转换成mp4&#xff1f;webm格式的视频也是最近几年也越来越多的&#xff0c;小编最近就不止一次的下载到过webm格式的视频&#xff0c;很多小伙伴肯定对它还并不是很了解&#xff0c;webm是由谷歌公司所提出以及开发出来的视频文件格式&#…

matlab读取STK生成的报告

一、STK 和 Matlab的生成的图片对比 &#xff08;一&#xff09;STK图片 &#xff08;二&#xff09;Matlab图片 &#xff08;三&#xff09;STK生成的报表数据 "Time (UTCG)","Azimuth (deg)","Elevation (deg)","Range (km)" 20 J…

编译Android平台的OpenCV库并启用OpenCL及Contrib

1.下载好OpenCV与OpenCV_Contirb 版本: 4.7 编译主机系统: Ubuntu 20.04 LTS 准备环境与工具: ANDRIOD SDK 与 NDK ,CMAKE ,NINJA ,GCC,G++ ,MAKE 开始编译: ../opencv/platforms/android/build_sdk.py --extra_modules_path=../opencv_contrib/modules --no_samples_bu…