常见的锁策略和synchronized的锁机制
自定义样式通知
文章目录
mysql
一. 常见的锁策略
1. 乐观锁和悲观锁
乐观锁和悲观锁主要是看主要是锁竞争的激烈程度.
elasticsearch
- 乐观锁预测锁竞争不是很激烈, 做的准备工作相对更少, 开销更小, 效率更高.
- 悲观锁预测锁竞争会很激烈, 做的准备工作相对更多, 开销更大, 效率更低.
🍂举个例子:
sed命令使用详解
比如在疫情期间, 我们谁也不知道下一步疫情的情况, 疫情一旦严重, 吃饭都成问题, 可能会买不到菜!
毕设选题
悲观锁, 就是在认为当前时刻可能就会出现这样的情况, 就需要提前准备, 所以去超市菜场大量的屯粮屯药, 以备不时之需;
sdk
乐观锁, 就是认为在国家的管控下, 疫情正常的衣食不会有太大的影响, 就不去屯货了.
性能优化
🍂应用场景:
相册图像识别
乐观锁一般应用在线程的冲突比较少, 也就是说读操作比较多, 写操作比较少; 而悲观锁则相反, 一般应用在线程冲突比较多, 写操作比较多的情况下.
默认浏览器
2. 轻量级锁和重量级锁
轻量级锁和重量级锁看的是锁操作的开销大不大.
实战演练项目
- 重量级锁: 加锁解锁的开销比较的大, 需要在内核态中进行完成, 多数情况下悲观锁是一个重量级锁, 但并不绝对.
- 轻量级锁: 它的开销比较小, 在用户态中完成就行, 多数情况下乐观锁是一个轻量级锁, 但并不绝对.
轻量级锁一般都是通过版本号机制或CAS算法进行实现的, 但对用重量级锁来说不是, 这与操作系统的内核有关, cpu一般会提供一些特殊指令, 操作系统会对这些指令进行封装一层, 提供一个mutex
(互斥量), 在Linux种就会提供一个这样的接口供用户进行加锁解锁; 一般来说, 如果锁是通过调用mutex
来进行实现的, 那么这个锁就是一个重量级锁.
多表查询
3. 自旋锁和挂起等待锁
- 自旋锁: 是一种典型的轻量级锁, 在线程获取不到锁的情况下, 不会立刻放弃CPU, 而是会一直快速频繁进行获取, 直到获取到锁; 这种策略的优势在于节省线程调度的开销并且更能及时的获取到锁, 缺点就是更加浪费cpu资源.
- 挂起等待锁: 是一种典型的重量级锁, 线程在获取不到锁的情况下会堵塞等待(放弃CPU,进入等待队列), 然后等到锁被释放的时候再有操作系统调度.
🍂举个例子:
高并发
想象一下, 去追求一个女神, 当男生向女神表白后, 女神说: 你是个好人, 但是我有男朋友了.
JNI函数签名
挂起等待锁: 继续等, 等女神分手, 过了很久很久之后, 突然女神发来消息, “咱俩要不试试?”, 但这个很长的时间间隔后女神想起了你, 这期间已经是沧海桑田了, 女神可能已经换了好几个男票了.
软件设计
自旋锁: 死皮赖脸的继续追求, 仍然每天持续的和女神说早安晚安, 一旦女神和上一任分手, 那么就能立刻抓住机会上位.
Mipmap
4. 普通互斥锁和读写锁
对于Java中synchronized
这样的锁就是一个普通的互斥锁, 只涉及两个操作:
移动开发
- 加锁.
- 解锁.
之所以有读写锁的设置是基于一个事实, 多线程针对同一个变量并发读, 这个时候没有线程安全问题的, 也不需要加锁控制, 对于读写锁来说, 分成了三个操作:
Qt控件
- 加读锁:如果代码只进行了读操作,就加读锁
- 加写锁:如果代码进行了修改操作,就加写锁
- 解锁: 针对读锁和读锁之间,是不存在互斥关系的
读锁和读锁之间是没有互斥的, 读锁和写锁之间, 写锁和写锁之间, 才需要互斥
哈希冲突
在java中有读写锁的标准类, 位于java.util.concurrent.locks.ReentrantReadWriteLock
, 其中ReentrantReadWriteLock.ReadLock
为读锁, ReentrantReadWriteLock.WriteLock
为写锁.
热更新
🍂应用场景:
pyspark
很多开发场景中, 读操作非常高频, 比写操作的频率高很多, 如果我们所有场景都使用同一种锁, 就会浪费一些资源, 而如果使用读写锁将读操作和写操作分开加锁, 就可以避免一些不必要的开销.
微信小程序自定义tabbar
5. 公平锁和非公平锁
- 公平锁: 多个线程在等待一把锁的时候, 遵循先来后到原则, 谁是先来的, 谁就先获得这把锁.
- 非公平锁: 多个线程等待同一把锁, 不遵守先来后到原则, 每个人等待线程获取锁的概率是均等的.
操作系统中原生的锁都是 “非公平锁”, 操作系统中的针对加锁的控制, 是依赖于线程调度顺序的, 这个调度顺序是随机的, 不会考虑到这个线程等待锁多久了, 如果想要实现公平锁, 就得在这个基础上加额外的数据结构(比如引入一个队列), 来记录线程的先后顺序.
安装solidity编译器
6. 可重入锁和不可重入锁
- 可重入锁: 一个线程针对同一把锁连续加锁两次,不会出现死锁
- 不可重入锁: 一个线程针对同一把锁连续加锁两次,会出现死锁
二. synchronized的锁机制
结合上面的锁策略, Synchronized
具有如下特性 性(只考虑 JDK 1.8):
匹配本地存储的组员名
- 既是乐观锁也是悲观锁, 当锁竞争较小时它就是乐观锁(默认), 锁竞争较大时它就是悲观锁.
- 是普通互斥锁。
- 既是轻量级锁(默认)也是重量级锁, 根据锁竞争激烈程度自适应.
- 轻量级锁部分基于自旋锁实现, 重量级锁部分基于挂起等待锁实现.
- 是非公平锁.
- 是可重入锁.
1. 锁升级/锁膨胀
JVM 将 synchronized 锁分为 无锁, 偏向锁, 轻量级锁, 重量级锁 状态; 会根据情况, 进行依次升级.
电脑无法调试手机
配置中心
- 当没有线程加锁的时候, 此时为无锁状态.
- 当首个线程进行加锁的时候, 此时进入偏向锁的状态, 偏向锁不是真的加锁, 而是在对象头做个标记(这个过程是非常轻量的).
🍂举个例子理解偏向锁:
webstorm
这里我的人设是一个妹子, 我谈了一个小哥哥, 长的又帅又有钱, 如果时间长了, 就想换换把他甩了, 但是他要是对我纠缠不休, 这就很麻烦.
基本类型包装类
于是我就调整了更高效谈恋爱的方式, 我就只是和这个小哥哥搞暧昧, 不明确我们彼此的关系, 这样做的好处就是有朝一日, 我想换男朋友了, 就直接甩了就行, 毕竟我们有情侣之实而无情侣之名, 这样换人的成本就很低了.
ruby
但是如果在这个过程中, 有另外一个妹子, 也在对这个小哥哥频频示好, 我就需要提高警惕了, 对于这种情况, 由于我和小哥哥前面感情铺垫到位了, 就可以立即顺理成章的和小哥哥官宣确认情侣关系(加锁), 并且勒令小哥哥和这个妹子离远点.
数据库架构
- 偏向锁并不是真的加锁, 只是做了一个标记, 这样带来的好处就是, 后续如果没人竞争的时候, 就一直不去确立关系(节省了确立关系/分手的开销), 如果没有其他的线程来竞争这个锁, 就不必真的加锁(节省了加锁解锁的开销), 如果在执行代码过程中, 并没有其他线程来尝试加锁, 那么在执行完synchronized之后, 取消偏向锁即可.
上述过程, 就类似于 “偏向锁” 这个过程, 相当于 “懒汉模式” 中的懒加载一样, “非必要,不加锁”.
学习
- 当有其他线程进行加锁, 导致产生了锁竞争时, 此时进入轻量级锁状态(迅速的把偏向锁升级成真正的加锁状态).
此处的轻量级锁是通过 CAS
来实现的, 如果其他的线程很快的释放锁, 那么我们的自旋锁是非常合适的, 但是如果其他线程长时间占用锁, 自旋锁就不太合适了, 自旋操作是一直让 CPU 空转, 比较浪费 CPU 资源, 因此此处的自旋不会一直持续进行, 而是达到一定的时间/重试次数, 就不再自旋了, 也就是所谓的 “自适应”.
- 如果竞争进一步加剧, 进入重量级锁状态(挂起等待锁).
重量级锁是基于操作系统原生的一组API(mutex)来进行加锁了, 当我们自旋不能快速获取到锁时, 锁竞争加剧, 就会升级为重量级锁, 我们的线程就会被放到阻塞队列中, 暂时不参与CPU调度, 当锁被释放了之后, 线程才有机会被调度, 从而有机会获取到锁, 一旦线程被切换出cpu, 这就是比较低效的事情了.
2. 锁消除
锁消除是编译器的智能判定, 有些代码, 编译器认为没有加锁的必要, 就会自动把你加的锁自动去除, 比如字符串相关的线程安全类StringBuffer
, 这个类中的关键方法都带有synchronized
, 但是当我们在单线程环境下使用StringBuffer, 不会涉及到线程安全问题, 此时编译器就会直接把这些加锁操作去除了.
3. 锁粗化
锁粗化就是将synchronized
的加锁代码块范围增大, 加锁的代码块中的代码越多, 锁的粒度就越粗, 否则锁的粒度就越细.
通常情况下, 认为锁的粒度细一点比较好, 加锁的部分的代码, 是不能并发执行的, 锁的粒度越细, 能并发的代码就越多, 反之就越少.
但是有些情况下, 锁的粒度粗一些反而更好, 如果我们两次加锁解锁的时间间隙非常小, 分开加锁会造成额外的资源开销, 而且中间间隙很小, 就算并发效果也不是很明显, 这种情况下不如直接一把大锁搞定.
🍂举个例子理解锁粗化 :
下属向领导汇报工作:
方式一:
第一次打电话, 汇报工作1, 挂电话.
第二次打电话, 汇报工作2, 挂电话.
第三次打电话, 汇报工作3, 挂电话.
方式二:
打电话, 汇报工作1, 2, 3, 挂电话, 显然, 相对于方式以, 这里的方案更高效.