草庐IT

【多线程进阶】各种锁策略以及常见的面试题

良辰针不戳 2023-07-28 原文

前言:
大家好,我是良辰丫,今天我们进入多线程进阶的部分,这个章节我们主要针对面试,面试是我们进公司重要的一部分,学习多线程进阶我们需要加上自己的理解(便于记忆),有的东西我们需要去记忆,毕竟是面试八股文,哈哈,废话不多说,开始我们的学习.💞💞💞

🧑个人主页:良辰针不戳
📖所属专栏:javaEE初阶
🍎励志语句:生活也许会让我们遍体鳞伤,但最终这些伤口会成为我们一辈子的财富。
💦期待大家三连,关注,点赞,收藏。
💌作者能力有限,可能也会出错,欢迎大家指正。
💞愿与君为伴,共探Java汪洋大海。

关于锁策略

1. 常见的锁策略

所谓的锁策略,就是解决问题的方式,如何去使用一把锁,让程序更好的运行.各种编程语言都有锁策略.
简述一下java的线程调度算法,java本身不负责调度,系统自己调度,操作系统的调度线程,有一个O(1)的调度算法(一个类似于哈希表的结构),所谓的随机调度不是数学上等概率的随机,而是具有一定的规则,但是程序干预不了这种规则,就近似认为是随机的了.

2. 乐观锁vs悲观锁

锁的实现者,预测接下来锁的冲突概率是大还是小,根据锁的冲突概率,定义了乐观锁与悲观锁的概念.这里的锁的冲突指的是锁竞争,两个线程针对同一个对象加锁,产生阻塞等待.

乐观锁 : 预测接下来冲突概率不大,假设数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做.

悲观锁 : 预测接下来的冲突概率比较大,总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这
样别人想拿这个数据就会阻塞直到它拿到锁。

  • 上述两种锁没有绝对的谁好谁坏,要根据一定的实际进行分析.
  • synchronized 初始使用乐观锁策略. 当发现锁竞争比较频繁的时候, 就会自动切换成悲观锁策略.
  • 乐观锁的一个重要功能就是要检测出数据是否发生访问冲突. 我们可以引入一个 “版本号” 来解决. 多线程修改余额可能会出错,使用版本号可以有效的解决问题.

3. 轻量级锁vs重量级锁

  • 轻量级锁 : 加锁解锁过程更快更高效,少量等待用户态和内核态切换,不太容易引发线程调度.
  • 重量级锁 : 加锁解锁过程更慢更低效,大量的内核态和用户态切换,很大可能引起线程调度.

一个乐观锁也可能是一个轻量级锁,一个悲观锁也可能是一个重量级锁.

4. 自旋锁vs挂起等待锁

按之前的方式,线程在抢锁失败后进入阻塞状态,放弃 CPU,需要过很久才能再次被调度. 但实际上, 大部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。没必要就放弃 CPU. 这个时候就可以使用自旋锁来处理这样的问题. 如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止. 第一次获取锁失败, 第二次的尝试会在极短的时间内到来. 一旦锁被其他线程释放, 就能第一时间获取到锁.

自旋锁是轻量级锁的一种典型实现,通常是纯用户态,不需要经过内核态,相对时间更短.

自旋锁的优缺点

  • 优点: 没有放弃 CPU, 不涉及线程阻塞和调度, 一旦锁被释放, 就能第一时间获取到锁.
  • 缺点: 如果锁被其他线程持有的时间比较久, 那么就会持续的消耗 CPU 资源. (而挂起等待的时候是不消耗 CPU 的).

挂起等待锁是重量级锁的一种体现,通过内核的机制来实现挂起等待,时间更长了.

5. 互斥锁vs读写锁

synchronized是互斥锁,加锁是单纯的加锁,没有更细化的区分了,没有更细化的区分了,synchronized只有两个操作,进入代码块加锁,出了代码块解锁.

多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需要进行互斥。如果两种场景下都用同一个锁,就会产生极大的性能损耗。所以读写锁因此而产生。

读写锁 : 读写锁是一种分开的锁,包括给读加锁,给写加锁和解锁,如果多个线程读同一个变量,不会涉及到线程安全问题.(读者之间并不互斥,而写者则要求与任何人互斥。)
在读写锁中:

  • 读锁和读锁之间,不会产生锁竞争,不会产生阻塞等待,不互斥.(无锁竞争,代码跑得快)
  • 写锁和写锁之间有锁竞争,互斥(减慢速度,保证准确性)
  • 读锁和写锁之间,有锁竞争,互斥(减慢速度,但是保证准确性)

注意:
注意, 只要是涉及到 “互斥”, 就会产生线程的挂起等待. 一旦线程挂起, 再次被唤醒就不知道隔了多久了. 因此尽可能减少 “互斥” 的机会, 就是提高效率的重要途径.

标准库提供了另外两个专门的读写锁(读锁是一个类,写锁是一个类)

  • ReentrantReadWriteLock.ReadLock类表示一个读锁,这个对象提供了lock/unlock方法进行加锁解锁.
  • ReentrantReadWriteLock.WriteLock类表示一个写锁,这个对象也提供了lock/unlock方法进行加锁解锁.

6. 可重入锁与不可重入锁

一个锁在一个线程中,连续对该锁加锁两次,不死锁就叫可重入锁;死锁了就叫不可重入锁.在java中基本不会出现不可重入锁的情况,因为咱们经常用的synchronized是一个可重入锁,加锁的时候会判定一下,看当前尝试申请锁的线程是不是已经就是锁的拥有者了,如果是,就不会再次进行加锁.

可重入锁顾名思义是允许同一个线程多次获取同一把锁.

7. 公平锁和非公平锁

我们在这里约定公平与非公平锁按照先来后到进行区分.
公平锁遵循先来后到的原则.
非公平锁不遵循先来后到的原则.

注意:

  • 对于synchronized是非公平锁.
  • 操作系统内部的线程调度就可以视为是随机的. 如果不做任何额外的限制, 锁就是非公平锁. 如果要想实现公平锁, 就需要依赖额外的数据结构, 来记录线程们的先后顺序.
  • 公平锁和非公平锁没有好坏之分, 关键还是看适用场景.

8. synchronized属于哪种锁

synchronized既是乐观锁,也是悲观锁,既是轻量级锁,也是重量级锁,轻量级锁部分基于自旋锁实现,重量级素部分基于挂起等待锁实现.synchronized会根据当前锁竞争的激烈程度,自适应,如果锁冲突不激烈,以轻量级/乐观锁的状态运行;如果锁冲突激烈,以重量级锁/悲观锁的状运行.

  • 既是;乐观锁,也是悲观锁
  • 既是轻量级锁,也是重量级锁
  • 轻量级锁基于自旋锁实现,重量级锁基于挂起等待实现.
  • 不是读写锁
  • 是可重入锁
  • 是非公平锁

9. 关于锁的常见面试题

  1. 你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?
  • 悲观锁认为多个线程访问同一个共享变量冲突的概率较大, 会在每次访问共享变量之前都去真正加锁.
  • 乐观锁认为多个线程访问同一个共享变量冲突的概率不大. 并不会真的加锁, 而是直接尝试访问数据. 在访问的同时识别当前的数据是否出现访问冲突.
  • 悲观锁的实现就是先加锁(比如借助操作系统提供的 mutex), 获取到锁再操作数据. 获取不到锁就等待.
  • 乐观锁的实现可以引入一个版本号. 借助版本号识别出当前的数据访问是否冲突.
  1. 介绍下读写锁?
  • 读写锁就是把读操作和写操作分别进行加锁.
  • 读锁和读锁之间不互斥.
  • 写锁和写锁之间互斥.
  • 写锁和读锁之间互斥.
  • 读写锁最主要用在 “频繁读, 不频繁写” 的场景中.
  1. 什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么?
  • 如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止. 第一次获取锁失败, 第二次的尝试会在极短的时间内到来. 一旦锁被其他线程释放, 就能第一时间获取到锁.
  • 相比于挂起等待锁,
    优点: 没有放弃 CPU 资源, 一旦锁被释放就能第一时间获取到锁, 更高效. 在锁持有时间比较短的场景下非常有用.
    缺点: 如果锁的持有时间较长, 就会浪费 CPU 资源.
  1. synchronized 是可重入锁么?

是可重入锁.
可重入锁指的就是连续两次加锁不会导致死锁.
实现的方式是在锁中记录该锁持有的线程身份, 以及一个计数器(记录加锁次数). 如果发现当前加锁的线程就是持有锁的线程, 则直接计数自增.

10. 死锁

关于死锁的情况.

  • 一个线程,一把锁,不可重入锁是死锁.
  • 两个线程两把锁,即使是可重入锁也会死锁.

10.1 哲学家问题

关于哲学家问题非常经典,大家需要重点关注一下.

  • N个线程,M把锁,线程数量和锁数量越多,就更容易死锁了.
  • 五个哲学家随机的进行拿起筷子吃面条或者思考人生.
  • 如果每个人都拿起一根筷子,就不能进行吃面条操作,他们会进入等待状态,等待的时候不会放下手中的筷子.
  • 出现上述的情况,就会产生死锁,每个哲学家都吃不到面条,也不会放下手中的筷子.

10.2 产生死锁的必要条件

  • 互斥使用:一个线程拿到一把锁后,另一个线程不能使用这把锁(锁的基本特点)
  • 不可抢占:一个线程拿到锁后,只能自己主动释放,不能被其它线程强行占有
  • 请求和保持:“吃着碗里的惦记锅里的”.(代码的特点)
  • 循环等待:逻辑依赖于循环的,例如"钥匙锁车里了,车钥匙锁家里了"

注意:
所谓必要条件是缺一不可的,也就是四个条件同时满足才能产生死锁.

10.3 如何避免死锁

死锁是一种严重的bug,我们应该采取一定的办法尽其所能避免死锁.
一个简单有效的办法,针对锁进行编号,如果需要同时获取多把锁,约定加锁顺序,先对小的编号进行加锁,后对大的编号进行加锁.
根据实际情况避免死锁,从产生死锁的必要条件入手.

有关【多线程进阶】各种锁策略以及常见的面试题的更多相关文章

  1. ruby - 什么是填充的 Base64 编码字符串以及如何在 ruby​​ 中生成它们? - 2

    我正在使用的第三方API的文档状态:"[O]urAPIonlyacceptspaddedBase64encodedstrings."什么是“填充的Base64编码字符串”以及如何在Ruby中生成它们。下面的代码是我第一次尝试创建转换为Base64的JSON格式数据。xa=Base64.encode64(a.to_json) 最佳答案 他们说的padding其实就是Base64本身的一部分。它是末尾的“=”和“==”。Base64将3个字节的数据包编码为4个编码字符。所以如果你的输入数据有长度n和n%3=1=>"=="末尾用于填充n%

  2. ruby - RuntimeError(自动加载常量 Apps 多线程时检测到循环依赖 - 2

    我收到这个错误:RuntimeError(自动加载常量Apps时检测到循环依赖当我使用多线程时。下面是我的代码。为什么会这样?我尝试多线程的原因是因为我正在编写一个HTML抓取应用程序。对Nokogiri::HTML(open())的调用是一个同步阻塞调用,需要1秒才能返回,我有100,000多个页面要访问,所以我试图运行多个线程来解决这个问题。有更好的方法吗?classToolsController0)app.website=array.join(',')putsapp.websiteelseapp.website="NONE"endapp.saveapps=Apps.order("

  3. ruby - 是否有用于序列化和反序列化各种格式的对象层次结构的模式? - 2

    给定一个复杂的对象层次结构,幸运的是它不包含循环引用,我如何实现支持各种格式的序列化?我不是来讨论实际实现的。相反,我正在寻找可能会派上用场的设计模式提示。更准确地说:我正在使用Ruby,我想解析XML和JSON数据以构建复杂的对象层次结构。此外,应该可以将该层次结构序列化为JSON、XML和可能的HTML。我可以为此使用Builder模式吗?在任何提到的情况下,我都有某种结构化数据-无论是在内存中还是文本中-我想用它来构建其他东西。我认为将序列化逻辑与实际业务逻辑分开会很好,这样我以后就可以轻松支持多种XML格式。 最佳答案 我最

  4. 【鸿蒙应用开发系列】- 获取系统设备信息以及版本API兼容调用方式 - 2

    在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList​()Obt

  5. git使用常见问题(提交代码,合并冲突) - 2

    文章目录git常用命令(简介,详细参数往下看)Git提交代码步骤gitpullgitstatusgitaddgitcommitgitpushgit代码冲突合并问题方法一:放弃本地代码方法二:合并代码常用命令以及详细参数gitadd将文件添加到仓库:gitdiff比较文件异同gitlog查看历史记录gitreset代码回滚版本库相关操作远程仓库相关操作分支相关操作创建分支查看分支:gitbranch合并分支:gitmerge删除分支:gitbranch-ddev查看分支合并图:gitlog–graph–pretty=oneline–abbrev-commit撤消某次提交git用户名密码相关配置g

  6. 阿里云国际版免费试用:如何注册以及注意事项 - 2

    作为新的阿里云用户,您可以50免费试用多种优惠,价值高达1,700美元(或8,500美元)。这将让您了解和体验阿里云平台上提供的一系列产品和服务。如果您以个人身份注册免费试用,您将获得价值1,700美元的优惠。但是,如果您是注册公司,您可以选择企业免费试用,提交基本信息通过企业实名注册验证,即可开始价值$8,500的免费试用!本教程介绍了如何设置您的帐户并使用您的免费试用版。​关于免费试用在我们开始此试用之前,您还必须遵守以下条款和条件才能访问您的免费试用:只有在一年内创建的账户才有资格获得阿里云免费试用。通过此免费试用优惠,用户可以免费试用免费试用活动页面上列出的每种产品一次。如果您有多个帐

  7. Hive SQL 五大经典面试题 - 2

    目录第1题连续问题分析:解法:第2题分组问题分析:解法:第3题间隔连续问题分析:解法:第4题打折日期交叉问题分析:解法:第5题同时在线问题分析:解法:第1题连续问题如下数据为蚂蚁森林中用户领取的减少碳排放量iddtlowcarbon10012021-12-1212310022021-12-124510012021-12-134310012021-12-134510012021-12-132310022021-12-144510012021-12-1423010022021-12-154510012021-12-1523.......找出连续3天及以上减少碳排放量在100以上的用户分析:遇到这类

  8. ruby - 如何让Ruby捕获线程中的语法错误 - 2

    我正在尝试使用ruby​​编写一个双线程客户端,一个线程从套接字读取数据并将其打印出来,另一个线程读取本地数据并将其发送到远程服务器。我发现的问题是Ruby似乎无法捕获线程内的错误,这是一个示例:#!/usr/bin/rubyThread.new{loop{$stdout.puts"hi"abc.putsefsleep1}}loop{sleep1}显然,如果我在线程外键入abc.putsef,代码将永远不会运行,因为Ruby将报告“undefinedvariableabc”。但是,如果它在一个线程内,则没有错误报告。我的问题是,如何让Ruby捕获这样的错误?或者至少,报告线程中的错误?

  9. ruby - 如何在 ruby​​ 中运行后台线程? - 2

    我是ruby​​的新手,我认为重新构建一个我用C#编写的简单聊天程序是个好主意。我正在使用Ruby2.0.0MRI(Matz的Ruby实现)。问题是我想在服务器运行时为简单的服务器命令提供I/O。这是从示例中获取的服务器。我添加了使用gets()获取输入的命令方法。我希望此方法在后台作为线程运行,但该线程正在阻塞另一个线程。require'socket'#Getsocketsfromstdlibserver=TCPServer.open(2000)#Sockettolistenonport2000defcommandsx=1whilex==1exitProgram=gets.chomp

  10. ruby - Rails 开发服务器、PDFKit 和多线程 - 2

    我有一个使用PDFKit呈现网页的pdf版本的Rails应用程序。我使用Thin作为开发服务器。问题是当我处于开发模式时。当我使用“bundleexecrailss”启动我的服务器并尝试呈现任何PDF时,整个过程会陷入僵局,因为当您呈现PDF时,会向服务器请求一些额外的资源,如图像和css,看起来只有一个线程.如何配置Rails开发服务器以运行多个工作线程?非常感谢。 最佳答案 我找到的最简单的解决方案是unicorn.geminstallunicorn创建一个unicorn.conf:worker_processes3然后使用它:

随机推荐