公平锁和非公平锁的区别?
👨🎓面试官:请你说说公平锁和非公平锁的区别?
🧑我:好的面试官。
锁的实现本质上都对应着一个入口的等待队列。如果一个线程没有获得锁,就会进入等待队列,当有线程释放锁的时候,就需要从等待队列中唤醒一个等待的线程,如果是公平锁,唤醒的策略就是谁等待的时间长,就唤醒谁,也就意味着谁就能抢占到锁资源。如果是非公平锁,则不提供这个公平保证,有可能等待时间短的反而被优先唤醒。
公平锁
公平锁:多个线程按照申请锁的顺序去获得锁,线程会按顺序进入队列,永远是队列第一位先获得锁。
- 优点:所有的线程都能得到资源,不会饿死在队列中。
- 缺点:吞吐量会下降很多,队列里面处理第一个线程,其他线程都会被阻塞,
cpu
唤醒阻塞线程的开销会很大。
非公平锁
非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。
- 优点:减少
cpu
唤醒线程的开销,整体的吞吐率会提高,cpu
不必唤醒所有线程,会减少唤醒的线程数,大大降低了线程上下文切换带来的时间损耗。 - 缺点:可能会导致队列中的线程一直长时间获取不到锁,导致线程饿死。
✔测试统计发现:10个线程,每个线程获取100000次锁,通过vmstat统计测试运行时系统线程上下文切换的耗时,发现公平锁和非公平锁对比,总耗时是其94.3倍,总切换次数是其133倍,可以看出,公平性锁保证了锁的获取按照FIFO原则,而代价是进行大量的线程切换;非公平是虽然可能造成线饥饿,但极少的线程切换,保证系统更大的吞吐率。
ReentrantLock
中就有公平锁和非公平锁的实现。默认是采用非公平锁的策略来实现锁的竞争逻辑,它内部是使用AQS
来实现所资源的竞争,没有竞争到锁资源的线程,会加入到AQS
的同步队列里,这个队列是一个FIFO
的双向链表。
🤔思考一下:
ReentrantLock
到底是如何实现锁的公平性和非公平性的呢?
下面一起探索ReentrantLock
的世界,分析它的实现原理:
首先先从Sync
类说起,Sync
类是ReentrantLock
的一个内部类,它继承了AbstractQueuedSynchronizer
,我们在执行锁的大部分操作,都是基于Sync
本身去实现的。
AbstractQueuedSynchronizer
也就是我们常说的AQS
,叫做 抽象队列同步器,它也是ReentrantLock
加锁释放锁的核心,该类提供了同步的核心实现,主要涵盖了以下几要素:
AQS
内部维护了一个volatile
修饰的共享变量,state
主要用来标记锁的状态。AQS
通过自定义Node
节点来维护一个队列,完成资源获取线程的排队工作。AQS
通过park
和unParkSuccessor
方法来实现阻塞和唤醒线程。AQS
内部的compareAndSetState
方法保证了锁状态设置的原子性。
AQS同步器的核心接口
// 获取当前同步状态
int getState();
// 设置当前同步状态
void setState(int newState);
// 使用CAS设置当前状态,该方法能够保证状态设置的原子性
boolean compareAndSetState(int expect, int update);
// 独占式获取锁
boolean tryAcquire(int arg);
// 独独占式释放锁
boolean tryRelease(int arg);
// 共享式获取锁
void doAcquireShared(int arg);
// 共享式释放锁
boolean tryReleaseShared(int arg);
下面分析一下AQS
的源码,看看它究竟是如何实现同步以及线程的阻塞和唤醒的:
/**
* AQS的内部内Node节点类
*/
static final class Node {
// 共享节点
static final Node SHARED = new Node();
// 排他节点
static final Node EXCLUSIVE = null;
// waitStatus=1,表示线程已取消
static final int CANCELLED = 1;
// waitStatus=-1,表示后继线程需要解停
static final int SIGNAL = -1;
// waitStatus=-2,表示线程正在等待状态
static final int CONDITION = -2;
// waitStatus=-3,表示下一个被获取对象应该是无条件传播
static final int PROPAGATE = -3;
// 等待状态
volatile int waitStatus;
// 当前节点的前任节点
volatile Node prev;
// 当前节点的后继节点
volatile Node next;
// 使该节点进入队列的线程。构造时初始化,使用后为空
volatile Thread thread;
}
默认ReentrantLock
采用的是非公平锁实现,下面来分析一次ReebtrantLock
加锁的过程吧,整体的过程描述如下:
- 当线程访问时,先判断
state
所标记值是否为0; - 发现
state
标识为0,接着将state
的值通过compareAndSetState()
方法修改为1; - 设置当前拥有独占访问权的线程为自己当前线程;
- 其他线程再次访问,也是一上来先去判断了一下
state
状态,发现是1,自然CAS
失败了,只能乖乖进入等待队列。
这时候线程B请求过来了,同样也是先判断state
状态,发现是1,那么CAS
失败,只能进入等待队列里等待。
经过一段时间,线程A访问资源结束,准备释放锁,修改state
状态为0,准备去唤醒B线程。
谁知道,这时候线程C也过来了,他也来抢占锁资源,发现state
为0,线程C果断CAS
成功,抢占了锁资源,还修改当前线程为自己。
此时线程B被A唤醒准备去获取锁,发现state
已经是1了,锁资源已经被抢占,结果线程B又只能默默回去等等队列继续等待了,真晦气🤢🤢🤢~
诺以上就是ReentrantLock
非公平锁的实现了,按照这样的话,线程B可能一直长时间无法获取到锁资源,但是,这样的非公平性设计,优点就是减少了线程切换等待时间,提高系统的吞吐量。
总结
ReentrantLock
默认采用了非公平锁策略来实现锁的竞争逻辑。它内部使用了AQS
同步器来实现锁资源的竞争,没有竞争到锁的线程,会加入到AQS
的同步队列里等待,实际上,ReentrantLock
和Synchronized
默认都是非公平锁,之所以如此设计,主要是为了减少像公平锁那样去阻塞等待带来的时间消耗,大大提高系统的性能。
好了,本篇文章介绍就到这里了,如果文章对你有所帮助,欢迎 点赞👍+评论💬+收藏❤,我是:👨austin流川枫,我们下期见!
转载自:https://juejin.cn/post/7198544446283104312