一、常见的锁策略
1.乐观锁 vs 悲观锁
锁的实现者,预测接下来的锁冲突概率大不大,根据这个概率决定接下来该做什么。
乐观锁:预测冲突不大,做的工作少一些,效率更高一些
悲观锁:预测冲突大,做的工作更多,效率低一些
举个生活中的例子:
在全国放开的时候,A认为既然都放开了,就不会存在再一次封地区,所以就没有任何准备;B认为疫情反复无常,有可能还会封地区,所以B依旧囤了很多药品和食物在家里面。
A就属于乐观锁,什么都没有准备。B就属于悲观锁,做了很多的准备工作。
2.轻量级锁 vs 重量级锁
轻量级锁:加锁解锁,过程更快更高效
1.少量的内核态用户态切换.
2.不太容易引发线程调度
重量级锁:加锁解锁,过程慢,低效
1.大量的内核态用户态切换
2.很容易引发线程的调度
内核态、用户态
假设在银行办理业务,需要你打印一份资料
1.你让工作人员帮你打印,虽然最后会帮你打印,但是你不能保证他中途没有其他的事情,而是直接给你打印资料。应用程序,在同一时刻要服务很多的程序。
2.你自己打印资料,你直接就能把资料打印了。
纯用户态操作,时间是可控的
涉及到了内核态,时间就不能控制
3.自旋锁 vs 挂起等待锁
自选锁:在获取锁失败后,即再次尝试获取锁, 无限循环, 直到获取到锁为止,一旦锁被其他线程释放, 就能第一时间获取到锁。是一种轻量级锁。
挂起等待锁:在获取锁失败后,会堵塞等待(放弃CPU,进入等待队列), 然后等到锁被释放的时候再有操作系统调度。是一种重量级锁。
例子:
在追求一个女神的时候,当男生向女神表白后, 女神说: 你是个好人, 但是我有男朋友了~~
挂起等待锁: 陷入沉沦不能自拔… 过了很久很久之后, 突然女神发来消息, “咱俩要不试试?” (注意,
这个很长的时间间隔里, 女神可能已经换了好几个男票了)。
自旋锁: 死皮赖脸坚韧不拔. 仍然每天持续的和女神说早安晚安. 一旦女神和上一任分手, 那么就能
立刻抓住机会上位。
自旋锁
优点: 没有放弃 CPU, 不涉及线程阻塞和调度, 一旦锁被释放, 就能第一时间获取到锁。
缺点: 如果锁被其他线程持有的时间比较久, 那么就会持续的消耗 CPU 资源. (而挂起等待的时候是
不消耗 CPU 的)。
4.互斥锁 vs 读写锁
互斥锁:进入synchroniced属于加锁,出synchroniced属于释放锁,两者不能同时发生。
读写锁:多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需要进行互斥。如果两种场景下都用同一个锁,就会产生极大的性能损耗
两个线程都只是读一个数据, 此时并没有线程安全问题. 直接并发的读取即可.
两个线程都要写一个数据, 有线程安全问题.
一个线程读另外一个线程写, 也有线程安全问题.
- 读锁和读锁:不会锁竞争,不会阻塞等待
- 写锁和写锁:有锁竞争
- 读锁和写锁:有锁竞争
5.可重入锁 vs 不可重入锁
- 可重入锁: 一个线程针对同一把锁连续加锁两次,不会出现死锁
- 不可重入锁: 一个线程针对同一把锁连续加锁两次,会出现死锁
这种属于不可重入锁
在java中, 并不容易出现死锁,synchroniced是可重入锁,加锁的时候会判断当前尝试加锁的线程是不是锁的拥有者,是就直接放行。
6.公平锁 vs 非公平锁
- 公平锁: 多个线程在等待一把锁的时候, 遵循先来后到原则, 谁是先来的, 谁就先获得这把锁.
- 非公平锁: 多个线程等待同一把锁, 不遵守先来后到原则, 每个人等待线程获取锁的概率是均等的.
公平锁
非公平锁
二、synchronized锁机制
1.synchronized的特点
- 既是乐观锁,也是悲观锁
- 既是轻量锁,也是重量级锁
- 轻量级锁基于自旋锁实现,重量级锁基于挂起等待锁实现
- 不是读写锁
- 是可重入锁
- 是非公平锁
2.锁升级
-
最开始加锁的时候是偏向锁
状态,只是让线程针对锁有个标记,如果整个代码执行过程没有其他的线程参与这个锁竞争,那么从始至终都是偏向锁,如果有线程来尝试竞争这个锁,偏向锁就会升级成真正的锁(自旋锁),这个时候其他的线程就只能阻塞等待。
-
自旋锁虽然执行速度很快,但是消耗大量的cpu,如果大量的线程参与锁竞争,为了较少cpu的消耗,就会升级成重量级锁。
偏向锁
只是做一个标记,没有其他线程来竞争锁,就永远不会升级
非必要,不加锁
3.锁消除
在编译阶段,检测当前代码是否是多线程执行、是否有必要加锁,如果没有必要,但是又有锁的存在,就会在编译过程中自动把锁去掉。
有些应用程序的代码中, 用到了 synchronized, 但其实没有在多线程环境下. (例如 StringBuffer)
StringBuffer sb = new StringBuffer();
sb.append("a");
sb.append("b");
sb.append("c");
sb.append("d");
此时每个 append 的调用都会涉及加锁和解锁. 但如果只是在单线程中执行这个代码, 那么这些加锁解锁操作是没有必要的, 白白浪费资源开销.
4.锁粗化
如果程序会频繁的加锁、解锁,编译器就可能把操作优化成一个粒度更粗的锁。
每次加锁解锁,都是有开销的,为了程序运行更加的高效,就引入了锁粗化
通过一次电话把所有事情汇报完毕明显比三次电话分别汇报的效率高了不少
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)