本文开始深度解析 线程底层的逻辑原理
1.常见锁策略
当我们需要自定义锁(标准库提供的锁 不够用) ——需要关注锁策略
一般情况下 synchronized 足矣覆盖绝大多数使用场景
锁策略 并不是和Java强相关,其他语言凡是涉及到并发编程,涉及到锁,都可以谈这样的锁策略
锁策略 可以简单认为是 ——这个锁在加锁的过程中 有什么特点 什么行为
1.1 悲观锁 乐观锁
描述的是加锁时 遇见的场景
不是针对某一种具体的锁,而是某个具体锁 具有“悲观” 特性或“乐观”特性
- 悲观:加锁的时候,预测接下来的锁竞争的情况非常激烈,需要针对这样的激烈情况做一些额外的工作
- 乐观:加锁的时候,锁竞争情况不激烈,不需要做额外工作
例如:
有一把锁 二十个线程尝试获取锁,每个线程加锁的频率都很高,一个线程加锁的时候,很可能锁被一个线程占用——悲观锁
有一把琐 假设只有两个线程线程尝试获取这把锁,每个线程加锁的频率都很低,一个线程加锁的时候,大概率另一个线程没有与其竞争——乐观锁
1.2 重量级锁 轻量级锁
遇到场景之后的解决方案
重量级锁,当悲观的场景下,此时就要付出更多的代价——更低效
轻量级锁,应对乐观的场景,此时付出的代价就会更小——更高效
1.3 挂起等待锁 自旋锁
挂起等待锁——重量级锁的典型实现
=> 操作系统内核级别,加锁的时候发现竞争,就会使该线程进入阻塞状态,后续就需要内核进行唤醒
自旋锁——轻量级锁的典型实现
=>应用程序级别,加锁的时候发现竞争,一般也不是进入阻塞,而是通过忙等的形式进行等待
(乐观锁的场景 本身遇见锁竞争的概率就很小,如果真的遇见竞争,在短时间内就能拿到锁)
例如 追女神 发现女神有对象 选择等待 有两种等待方式
1.等自己眼中的女神(竞争不激烈) =>乐观
等待女神,隔三岔五进行联系,会消耗一定CPU,过不了多久,就能有机会,只要发现机会就能上位,整个锁等待时间不长
即——获取锁的周期更短,及时获取锁,等待过程一直消耗CPU
2.等 公认的女神(竞争激烈)=>悲观
等待过程中不再联系,专心自己的事情,很长时间过去了,偶然听说女神单身的消息(此时女神可能分手好几次了),这个过程,不在女神身上消耗精力
即——获取锁的周期更长,很难及时获取锁,但这个过程不用一直消耗CPU,可以让CPU处理别的事情
悲观锁=>重量级锁=>挂起等待锁
乐观锁=>轻量级锁=>自旋锁
对于synchronized来说,属于自适应锁 JVM内部,会统计每个锁竞争的激烈情况
如果竞争不激烈,此时synchronized 就会按照 轻量级锁(自旋锁)
如果竞争很激烈,此时synchronized 就会按照 重量级锁(挂起等待锁)
1.4 普通互斥锁 读写锁
普通互斥锁——synchronized 加锁 解锁
读写锁—— 读方式加锁 写方式加锁 解锁
多线程读取一个数据,线程本身是安全的
多个线程读取,一个线程修改数据,肯定涉及线程安全问题
由于 服务器开发中,读多写少的场景十分常见
大部分操作在读,少部分操作在 写——一旦给 读 写都加上普通互斥锁,锁冲突会很严重
读写锁——确保 读锁和读锁之间不会互斥(不发生阻塞)
写锁和写锁之间 有互斥
写锁和读锁之间也有互斥
可以保证线程安全的前提下,降低锁冲突概率,提高效率
1.5 可重入锁 不可重入锁
synchronized 是“可重入锁”——一个线程一把锁,连续加锁多次,是否会死锁
核心要点:
- 锁要记录当前哪个线程持有这把锁
- 使用计数器,记录当前加锁次数,在合适的时候解锁
1.6 公平锁 非公平锁
synchronized是非公平锁
当女神和男朋友分手 谁上位才是公平的呢?
1.按照先来后到的顺序
2.概率均等
对锁来说,默认情况是概率均等,操作系统 对线程的调度是随机的
要想实现公平锁,需要付出额外的东西,
比如需要使用一个队列,记录一下,各个线程获取锁的顺序
1.7小结
对 synchronized 来说 属于
- 自适应的
- 普通互斥锁(不是读写锁)
- 可重入锁
- 非公平锁
2. synchronized的锁升级过程
自适应过程(单向)
无锁=>偏向锁=>自旋锁=>重量级锁
偏向锁是指,进入synchronized后,并没有真的加锁,——是一种懒汉模式的体现
先简单做一个轻量标记,相比于加锁解锁 效率更高
- 如果没有其他线程竞争此锁,最终当线程执行到解锁代码时 也只是清除标记,(不涉及真正的加锁解锁)
- 如果有其他线程竞争此锁,就抢先一步,宣誓主权,真加锁,偏向锁=>轻量锁 其他线程只能等待
升级情况:
代码进入synchronized 的代码块—— 无锁=>偏向锁
拿到偏向锁的线程运行过程,发现其他线程尝试竞争这个锁——偏向锁=>自旋锁
JVM发现当前竞争锁的情况非常激烈——自旋锁=>重量级锁
当前JVM只提供“锁升级” 不能“锁降级”
3.锁消除
是编译器优化的一种体现,编译器会判断,当前代码逻辑是否真的需要加锁,如果确实不需要,会自动忽视synchronized
锁消除是比较保守的,没有100%的把握是不会去除的
4.锁粗化
锁的粒度
加锁和解锁之间,包含的代码越多,认为锁的粒度越粗
如果包含代码越少,则认为锁的粒度越细
此处代码指的不是代码行数,而是实际执行的指令/时间
一个代码中,反复针对细粒度代码加锁,可能会被优化更粗粒度的代码加锁
例如给领导汇报工作,一共三件事,如果没有提前组织好语言,打一次电话汇报一件事情,一共打三次电话才汇报完毕,领导都厌烦了。
打电话的时候,领导不能做别的事情,全身心投入到汇报中,
如果没有锁粗化,是一种低效率浪费资源的做法

如图示,将三处细粒度代码 优化成一段粗粒度代码
2405

被折叠的 条评论
为什么被折叠?



