谷德设计网站官网入口,索牛网站建设,长春网站上排名,网站后台组成目录 #x1f334; 乐观锁 vs 悲观锁#x1f38d;重量级锁 vs 轻量级锁#x1f340;自旋锁#xff08;Spin Lock#xff09;#x1f38b;公平锁 vs ⾮公平锁#x1f333;可重⼊锁 vs 不可重⼊锁#x1f384;读写锁⭕相关面试题 常⻅的锁策略 注意: 接下来讲解的锁策略不… 目录 乐观锁 vs 悲观锁重量级锁 vs 轻量级锁自旋锁Spin Lock公平锁 vs ⾮公平锁可重⼊锁 vs 不可重⼊锁读写锁⭕相关面试题 常⻅的锁策略 注意: 接下来讲解的锁策略不仅仅是局限于 Java . 任何和 “锁” 相关的话题, 都可能会涉及到以下内容. 这些特性主要是给锁的实现者来参考的.
普通的程序猿也需要了解⼀些, 对于合理的使⽤锁也是有很⼤帮助的. 乐观锁 vs 悲观锁
悲观锁: 总是假设最坏的情况每次去拿数据的时候都认为别⼈会修改所以每次在拿数据的时候都会上锁 这样别⼈想拿这个数据就会阻塞直到它拿到锁。
乐观锁 假设数据⼀般情况下不会产⽣并发冲突所以在数据进⾏提交更新的时候才会正式对数据是否产⽣ 并发冲突进⾏检测如果发现并发冲突了则让返回⽤⼾错误的信息让⽤⼾决定如何去做。
乐观锁的一个重要功能就是要检测出数据是否发生访问冲突.
那我们具体是怎么检测的呢这里我们我们可以引入一个 “版本号” 来解决.
那什么是版本号呢请看下面的例子 假设我们需要多线程修改 “用户账户余额”.
设当前余额为 100. 引入一个版本号 version, 初始值为 1. 并且我们规定 提交版本必须大于记录当前版本”才能执行更新余额
接下来我们进行以下操作
第一步线程 A 此时准备将其读出 version1, balance100 线程 B 也读入此信息 version1,balance100 第二步线程 A 操作的过程中并从其帐户余额中扣除 50 100-50 线程 B 从其帐户余额中扣除 20 100-20 第三步线程 A 完成修改工作将数据版本号加1 version2 连同帐户扣除后余额 balance50写回到内存中; 第四步线程 B 完成了操作也将版本号加1 version2 试图向内存中提交数据 balance80但此时比对版本发现操作员 B 提交的数据版本号为 2 数据库记录的当前版本也为 2 不满足 “提交版本必须大于记录当前版本才能执行更新“ 的乐观锁策略。就认为这次操作失败.
在Java中Synchronized 初始使⽤乐观锁策略. 当发现锁竞争⽐较频繁的时候, 就会⾃动切换成悲观锁策略.
重量级锁 vs 轻量级锁
锁的核⼼特性 “原⼦性”, 这样的机制追根溯源是 CPU 这样的硬件设备提供的. • CPU 提供了 “原⼦操作指令”. • 操作系统基于 CPU 的原⼦指令, 实现了 mutex 互斥锁. • JVM 基于操作系统提供的互斥锁, 实现了 synchronized 和 ReentrantLock 等关键字和类. 注意, synchronized 并不仅仅是对 mutex 进⾏封装, 在 synchronized 内部还做了很多其 他的⼯作
重量级锁: 加锁机制重度依赖了 OS 提供了 mutex
• ⼤量的内核态⽤⼾态切换 • 很容易引发线程的调度 这两个操作, 成本⽐较⾼. ⼀旦涉及到⽤⼾态和内核态的切换, 就意味着 “沧海桑⽥”. 轻量级锁: 加锁机制尽可能不使⽤ mutex, ⽽是尽量在⽤⼾态代码完成. 实在搞不定了, 再使⽤ mutex.
• 少量的内核态⽤⼾态切换. • 不太容易引发线程调度.
什么是用户态什么是内核态 理解⽤⼾态 vs 内核态 想象去银⾏办业务. 在窗⼝外, ⾃⼰做, 这是⽤⼾态. ⽤⼾态的时间成本是⽐较可控的. 在窗⼝内, ⼯作⼈员做, 这是内核态. 内核态的时间成本是不太可控的. 如果办业务的时候反复和⼯作⼈员沟通, 还需要重新排队, 这时效率是很低的. synchronized 开始是⼀个轻量级锁. 如果锁冲突比较严重, 就会变成重量级锁.
自旋锁Spin Lock
按之前的⽅式线程在抢锁失败后进⼊阻塞状态放弃 CPU需要过很久才能再次被调度.
但实际上, ⼤部分情况下虽然当前抢锁失败但过不了很久锁就会被释放。没必要就放弃 CPU. 这个时候就可以使⽤⾃旋锁来处理这样的问题.
⾃旋锁伪代码: while (抢锁(lock) 失败) {} 如果获取锁失败, ⽴即再尝试获取锁, ⽆限循环, 直到获取到锁为⽌. 第⼀次获取锁失败, 第⼆次的尝试会在极短的时间内到来.
⼀旦锁被其他线程释放, 就能第⼀时间获取到锁 理解⾃旋锁 vs 挂起等待锁 想象⼀下, 去追求⼀个⼥神. 当男⽣向⼥神表⽩后, ⼥神说: 你是个好⼈, 但是我有男朋友了~~ 挂起等待锁: 陷⼊沉沦不能⾃拔… 过了很久很久之后, 突然⼥神发来消息, “咱俩要不试试?” (注意, 这 个很⻓的时间间隔⾥, ⼥神可能已经换了好⼏个男票了). ⾃旋锁: 死⽪赖脸坚韧不拔. 仍然每天持续的和⼥神说早安晚安. ⼀旦⼥神和上⼀任分⼿, 那么就能⽴刻 抓住机会上位. ⾃旋锁是⼀种典型的 轻量级锁 的实现⽅式. • 优点: 没有放弃 CPU, 不涉及线程阻塞和调度, ⼀旦锁被释放, 就能第⼀时间获取到锁. • 缺点: 如果锁被其他线程持有的时间⽐较久, 那么就会持续的消耗 CPU 资源. (⽽挂起等待的时候是不消耗 CPU 的).
synchronized 中的轻量级锁策略⼤概率就是通过⾃旋锁的⽅式实现的.
公平锁 vs ⾮公平锁
假设三个线程 A, B, C. A 先尝试获取锁, 获取成功. 然后 B 再尝试获取锁, 获取失败, 阻塞等待; 然后 C 也尝试获取锁, C 也获取失败, 也阻塞等待.
当线程 A 释放锁的时候, 会发⽣啥呢?
公平锁: 遵守 “先来后到”. B ⽐ C 先来的. 当 A 释放锁的之后, B 就能先于 C 获取到锁.
⾮公平锁: 不遵守 “先来后到”. B 和 C 都有可能获取到锁.
这就好⽐⼀群男⽣追同⼀个⼥神. 当⼥神和前任分⼿之后, 先来追⼥神的男⽣上位, 这就是公平锁; 如果 是⼥神不按先后顺序挑⼀个⾃⼰看的顺眼的, 就是⾮公平锁.
注意:
• 操作系统内部的线程调度就可以视为是随机的. 如果不做任何额外的限制, 锁就是⾮公平锁. 如果要 想实现公平锁, 就需要依赖额外的数据结构, 来记录线程们的先后顺序.
• 公平锁和⾮公平锁没有好坏之分, 关键还是看适⽤场景.
synchronized 是⾮公平锁.
可重⼊锁 vs 不可重⼊锁
可重⼊锁的字⾯意思是“可以重新进⼊的锁”即允许同⼀个线程多次获取同⼀把锁。
⽐如⼀个递归函数⾥有加锁操作递归过程中这个锁会阻塞⾃⼰吗如果不会那么这个锁就是可重⼊锁因为这个原因可重⼊锁也叫做递归锁。
Java⾥只要以Reentrant开头命名的锁都是可重⼊锁⽽且JDK提供的所有现成的Lock实现类包括synchronized关键字锁都是可重⼊的。
⽽ Linux 系统提供的 mutex 是不可重⼊锁.
理解 “把⾃⼰锁死” ⼀个线程没有释放锁, 然后⼜尝试再次加锁.
1 // 第⼀次加锁, 加锁成功
2 lock();
3 // 第⼆次加锁, 锁已经被占⽤, 阻塞等待.
4 lock();按照之前对于锁的设定, 第⼆次加锁的时候, 就会阻塞等待. 直到第⼀次的锁被释放, 才能获取到第⼆个 锁. 但是释放第⼀个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想⼲了, 也就⽆法进⾏解 锁操作. 这时候就会 死锁. 这样的锁称为 不可重⼊锁.
最后要记得
synchronized 是可重入锁
读写锁
多线程之间数据的读取⽅之间不会产⽣线程安全问题但数据的写⼊⽅互相之间以及和读者之间都 需要进⾏互斥。如果两种场景下都⽤同⼀个锁就会产⽣极⼤的性能损耗。所以读写锁因此⽽产⽣。
读写锁readers-writer lock看英⽂可以顾名思义在执⾏加锁操作时需要额外表明读写意图复数读者之间并不互斥⽽写者则要求与任何⼈互斥。
⼀个线程对于数据的访问, 主要存在两种操作: 读数据 和 写数据.
• 两个线程都只是读⼀个数据, 此时并没有线程安全问题. 直接并发的读取即可. • 两个线程都要写⼀个数据, 有线程安全问题. • ⼀个线程读另外⼀个线程写, 也有线程安全问题.
读写锁就是把读操作和写操作区分对待. Java 标准库提供了 ReentrantReadWriteLock 类, 实现 了读写锁
ReentrantReadWriteLock.ReadLock 类表⽰⼀个读锁. 这个对象提供了 lock / unlock ⽅法 进⾏加锁解锁.ReentrantReadWriteLock.WriteLock 类表⽰⼀个写锁. 这个对象也提供了 lock / unlock ⽅法进⾏加锁解锁
其中,
读加锁和读加锁之间, 不互斥.写加锁和写加锁之间, 互斥.读加锁和写加锁之间, 互斥.
注意, 只要是涉及到 “互斥”, 就会产⽣线程的挂起等待. ⼀旦线程挂起, 再次被唤醒就不知道隔了多久 了. 因此尽可能减少 “互斥” 的机会, 就是提⾼效率的重要途径
读写锁特别适合于 “频繁读, 不频繁写” 的场景中. (这样的场景其实也是⾮常⼴泛存在的). ⽐如学校的教务系统. 每节课⽼师都要使⽤教务系统点名, 点名就需要查看班级的同学列表(读操作). 这个操作可能要每周执 ⾏好⼏次. ⽽什么时候修改同学列表呢(写操作)? 就新同学加⼊的时候. 可能⼀个⽉都不必改⼀次. 再⽐如, 同学们使⽤教务系统查看作业(读操作), ⼀个班级的同学很多, 读操作⼀天就要进⾏⼏⼗次上 百次. 但是这⼀节课的作业, ⽼师只是布置了⼀次(写操作) Synchronized 不是读写锁.、
⭕相关面试题
你是怎么理解乐观锁和悲观锁的具体怎么实现呢 悲观锁认为多个线程访问同⼀个共享变量冲突的概率较⼤, 会在每次访问共享变量之前都去真正加锁. 乐观锁认为多个线程访问同⼀个共享变量冲突的概率不⼤. 并不会真的加锁, ⽽是直接尝试访问数据. 在访问的同时识别当前的数据是否出现访问冲突. 悲观锁的实现就是先加锁(⽐如借助操作系统提供的 mutex), 获取到锁再操作数据. 获取不到锁就等待. 乐观锁的实现可以引⼊⼀个版本号. 借助版本号识别出当前的数据访问是否冲突. (实现细节参考上⾯ 的图). 2.介绍下读写锁? 读写锁就是把读操作和写操作分别进⾏加锁. 读锁和读锁之间不互斥 写锁和写锁之间互斥. 写锁和读锁之间互斥. 读写锁最主要⽤在 “频繁读, 不频繁写” 的场景中.、 3.什么是⾃旋锁为什么要使⽤⾃旋锁策略呢缺点是什么 如果获取锁失败, ⽴即再尝试获取锁, ⽆限循环, 直到获取到锁为⽌. 第⼀次获取锁失败, 第⼆次的尝试 会在极短的时间内到来. ⼀旦锁被其他线程释放, 就能第⼀时间获取到锁. 相⽐于挂起等待锁, 优点: 没有放弃 CPU 资源, ⼀旦锁被释放就能第⼀时间获取到锁, 更⾼效. 在锁持有时间⽐较短的场景 下⾮常有⽤. 缺点: 如果锁的持有时间较⻓, 就会浪费 CPU 资源. 4.synchronized 是可重⼊锁么 是可重⼊锁. 可重⼊锁指的就是连续两次加锁不会导致死锁. 实现的⽅式是在锁中记录该锁持有的线程⾝份, 以及⼀个计数器(记录加锁次数). 如果发现当前加锁的 线程就是持有锁的线程, 则直接计数⾃增.