面试官:请你说说公平锁和非公平锁的区别?

👨‍🎓面试官:请你说说公平锁和非公平锁的区别?

🧑我:好的面试官。

锁的实现本质上都对应着一个入口的等待队列。如果一个线程没有获得锁,就会进入等待队列,当有线程释放锁的时候,就需要从等待队列中唤醒一个等待的线程,如果是公平锁,唤醒的策略就是谁等待的时间长,就唤醒谁,也就意味着谁就能抢占到锁资源。如果是非公平锁,则不提供这个公平保证,有可能等待时间短的反而被优先唤醒。

公平锁

公平锁:多个线程按照申请锁的顺序去获得锁,线程会按顺序进入队列,永远是队列第一位先获得锁。

  • 优点:所有的线程都能得到资源,不会饿死在队列中。
  • 缺点:吞吐量会下降很多,队列里面处理第一个线程,其他线程都会被阻塞,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通过parkunParkSuccessor方法来实现阻塞和唤醒线程。
  • 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的同步队列里等待,实际上,ReentrantLockSynchronized默认都是非公平锁,之所以如此设计,主要是为了减少像公平锁那样去阻塞等待带来的时间消耗,大大提高系统的性能。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值