文章目录
1. 乐观锁 vs 悲观锁
乐观锁 指的是预测锁竞争不是很激烈,也就是指这里做的工作相对较少。
悲观锁 指的是预测锁竞争会很激烈,也就是指指这里做的工作相对多一点。
悲观和乐观都不是绝对的,主要就是看预测锁竞争激烈程度的结论。
这两个锁是站在冲突概率的预测角度。
2. 互斥锁 vs 读写锁
互斥锁 就是之前使用的 synchronized 这样的锁,提供 加锁 和 解锁 两个操作。
如果一个线程加锁了,另一个线程也尝试加锁,就会阻塞等待。
读写锁 提供了三种操作:
- 针对读加锁
- 针对写加锁
- 解锁
基于一个事实:
多线程针对同一个变量并发读,这个时候是没有线程安全问题的,也不需要加锁控制。
读写锁 就是针对这种情况所采取的特殊处理。
当前的代码中,如果只是读操作,就加读锁即可,如果有写操作,就加写锁。
- 读锁 和 读锁 之间没有互斥
- 写锁 和 写锁 之间存在互斥
- 写锁 和 读锁 之间存在互斥
如果当前有一组线程都去读(加读锁),此时这些线程之间是没有锁竞争和线程安全问题的。
如果当前有一组操作有读也有写,才会有竞争问题。
另一个事实:
在很多开发场景中,读操作的频率比写操作的频率高很多。
3. 重量级锁 vs 轻量级锁
轻量级锁 加锁解锁开销比较小,效率更高;重量级锁 加锁解锁开销比较大,效率更低。
多数情况下,乐观锁也是一个轻量级锁(不能完全保证)
多数情况下,悲观锁也是一个重量级锁(不能完全保证)
这两个锁是站在加锁操作的开销角度。
4. 自旋锁 vs 挂起等待锁
自旋锁 是一种典型的轻量级锁,挂起等待锁 是一种典型的重量级锁。
举一个例子
张三要向一个女生表白,此时就是张三尝试对这个女生加锁。
但是女生说她有男友了,也就是说明她被别人加锁了。
如果张三还不死心,为了找一个机会上位,此时就有两种等待的方式。
-
自旋锁:
张三没有都在女生身边转悠,一旦女生分手了,就能第一时间知道。
而一旦锁被释放就能第一时间感知到,从而有机会获取到锁,很明显,自旋锁占用了大量的资源。 -
挂起等待锁:
张三愿意等女生分手,张三就悄悄地等着,也不会去打扰那个女生。
当女生分手的时候,有可能会想起张三,也有可能不会。但是更大的概率,女生会把张三也忘记了。
当真的被唤醒的时候,不知道过了多长时间。
此时就是把 CPU 省下来了,就可以干别的事情了。
5. 公平锁 vs 非公平锁
此处的公平定义为 “先来后到”
举个例子
女神现在已经被别人加锁了,1号老铁已经阻塞等待一年了,2号老铁阻塞等待一个月了,
而3号老铁从昨天才开始阻塞等待的。
公平锁 就是当女神分手的时候,最先开始追女神的1号老铁先和女神谈恋爱,
后续老铁等女神再次分手才可以和女神谈恋爱。
非公平锁 就是 “雨露均沾” 的意思。
当女神分手的时候,三个老铁一起上,公平竞争。
最早开始追女神的1号老铁所付出的时间和精力都比其他两个老铁要多,这是就不那么公平了。
就好比某个企业有一个晋升的名额,此时应该采取的是 “先来后到” 的方式才公平。
因为先来的员工工作时间长,对公司的贡献比较大,而不应该 “雨露均沾” 的方式,
有的员工工作时间长,有的工作时间短,这样就不公平了。
操作系统和 java synchronized 元素都是 “非公平锁”
操作系统这里的针对加锁的控制,本身就依赖于线程调度顺序的。
这个调度顺序是随机的,不会考虑到这个线程等待锁多久了。
要想实现公平锁,就得在这个基础上,能够引入额外的东西。
(引入一个队列,让这些加锁的线程去排队)
但是引入额外的队列又有额外的开销和代码上成本。
6. 可重入锁 vs 不可重入锁
可重入锁 :一个线程针对一把锁,连续多次加锁都不会死锁。
不可重入锁 :一个线程针对一把锁,连续两次加锁会出现死锁。
7. synchronized 是哪种锁
1、synchronized 既是一个 悲观锁 又是一个 乐观锁。
synchronized 默认是乐观锁,但是如果发现当前的锁竞争比较激烈,就会变成悲观锁。
2、synchronized 即是 轻量级锁 又是 重量级锁。
synchronized 默认是轻量级锁,但是如果发现当前的锁竞争比较激烈,就会变成重量级锁。
3、synchronized 这里的轻量级锁,是基于 自旋锁 的方式实现的。synchronized 这里的重量级锁,是基于挂起等待锁的方式实现的。
4、synchronized 不是 读写锁
5、synchronized 是 非公平锁
6、synchronized 是 可重入锁
8. 相关面试题
1、你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?
悲观锁 认为多个线程访问同一个共享变量冲突的概率较大,会在每次访问共享变量之前都去真正加锁。
乐观锁 认为多个线程访问同一个共享变量冲突的概率不大。并不会真的加锁, 而是直接尝试访问数
据。在访问的同时识别当前的数据是否出现访问冲突。
悲观锁 的实现就是先加锁(比如借助操作系统提供的 mutex),获取到锁再操作数据。
获取不到锁就等待。
乐观锁 的实现可以引入一个版本号,借助版本号识别出当前的数据访问是否冲突。
2、介绍下读写锁?
读写锁就是把读操作和写操作分别进行加锁.
- 读锁和读锁之间不互斥
- 写锁和写锁之间互斥
- 写锁和读锁之间互斥
读写锁最主要用在 “频繁读,不频繁写” 的场景中
3、什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么?
如果获取锁失败,立即再尝试获取锁,无限循环,直到获取到锁为止。
第一次获取锁失败,第二次的尝试会在极短的时间内到来。一旦锁被其他线程释放,就能第一时间获取到锁。
相比于挂起等待锁:
优点:
没有放弃 CPU 资源,一旦锁被释放就能第一时间获取到锁,更高效,在锁持有时间比较短的场
景下非常有用。
缺点:如果锁的持有时间较长, 就会浪费 CPU 资源。
4、synchronized 是可重入锁么?
是可重入锁。
可重入锁指的就是连续两次加锁不会导致死锁。
实现的方式是在锁中记录该锁持有的线程身份,以及一个计数器(记录加锁次数)。
如果发现当前加锁的线程就是持有锁的线程,则直接计数自增。