目录
终于进入到多线程的进阶了,这里面涉及到的内容面试容易考,但是工作中很少直接用到。
一、常见的锁策略:
注意:接下来讲解的锁策略不仅仅是局限于 Java 。任何和 "锁" 相关的话题,都可能会涉及到以下内容。这些特性主要是给锁的实现者来参考的。我们了解一些,也能更加合理的使用锁。
1.1 悲观锁 | 乐观锁:
加锁的时候,预测当前锁冲突的概率是大还是小。
• 悲观锁:
预测当前锁的冲突概率大,后续要做的工作往往就会更多。加锁的开销就会更大(时间,系统资源)。
• 乐观锁:
预测当前锁的冲突概率不大,后续要做的工作往往就会更少。加锁的开销就会更小(时间,系统资源)。
synchronized 初始使用乐观锁策略。当发现锁竞争比较频繁的时候,就会自动切换成悲观锁策略。所以 synchronized 既是乐观锁也是悲观锁,支持自适应。
1.2 重量级锁 | 轻量级锁:
一般来说,悲观锁往往就是重量级锁(加锁过程做的事情多),乐观锁往往就是轻量级锁(加锁过程做的事情少)。
锁的核心特性 "原子性",这样的机制追根溯源是 CPU 这样的硬件设备提供的。硬件有提供,软件层面才能实现。
• CPU 提供了 "原子操作指令"。
• 操作系统基于 CPU 的原子指令,实现了 mutex 互斥锁。
• JVM 基于操作系统提供的互斥锁,实现了 synchronized 和 ReentrantLock 等关键字和类。
注意:synchronized 并不仅仅是对 mutex 进行封装,在 synchronized 内部还做了很多其他的工作。
• 重量级锁:
加锁机制重度依赖了 OS 提供了 mutex。
这样做的特点有:1. 大量的内核态用户态切换。2. 很容易引发线程的调度。
这两个操作,成本比较高,一旦涉及到用户态和内核态的切换,就意味着 “沧海桑田”。
• 轻量级锁:
加锁机制尽可能不使用 mutex,而是尽量在用户态代码完成,实在搞不定了,再使用 mutex。
这样做的特点有:1. 少量的内核态用户态切换。2. 不太容易引发线程调度。
为什么会有这样的好处呢?举个栗子:
想象去银行办业务。在窗口外,自己做,这是用户态,用户态的时间成本是比较可控的。在窗口内让工作人员做,这是内核态,内核态的时间成本是不太可控的(可能人家处理一半,去做别的事情了)。如果办业务的时候反复和工作人员沟通,还需要重新排队,这时效率是很低的。
重量级锁、轻量级锁和悲观锁、乐观锁的概念有重合的地方,面试的时候要能转的过来。
synchronized 开始是一个轻量级锁。如果锁冲突严重,就会变成重量级锁。
1.3 自旋锁 | 挂起等待锁:
• 自旋锁:
按之前的方式,线程在抢锁失败后进入阻塞状态,放弃 CPU,需要过很久才能再次被调度。但实际上,大部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。没必要放弃 CPU,这个时候就可以使用自旋锁来处理这样的问题。
自旋锁伪代码:
while (抢锁(lock) == 失败) {}
⼀旦锁被其他线程释放,就能第⼀时间获取到锁(线程没有被调度)。
自旋锁是一种典型的轻量级锁的实现方式。
优点:没有放弃 CPU,不涉及线程阻塞和调度,⼀旦锁被释放,就能第一时间获取到锁。
缺点:如果锁被其他线程持有的时间比较久,那么就会持续的消耗 CPU 资源,CPU 在空转。
synchronized 中的轻量级锁策略大概率就是通过自旋锁的方式实现的。
• 挂起等待锁:
是重量级锁的一种典型的实现方式,借助系统中的线程调度机制,当尝试加锁,并且锁被占用了,出现锁冲突,就会让当前这个尝试加锁的线程,被挂起(阻塞状态)。此时这个线程就不会参与线程调度了。知道这个锁被释放,然后系统才能唤醒这个线程,去尝试重新获取锁。
1.4 公平锁 | 非公平锁:
• 公平锁:遵守 "先来后到"。B 比 C 先来的。当 A 释放锁的之后,B 就能先于 C 获取到锁。
• 非公平锁:不遵守 "先来后到"。B 和 C 都有可能获取到锁。
其实这两个策略都挺公平的,只是最初的 Java 大佬把先来后到定义成公平,均等机会定义成不