写点什么

排队打饭:公平锁和非公平锁 (面试)

用户头像
叫练
关注
发布于: 2021 年 01 月 03 日
排队打饭:公平锁和非公平锁(面试)

简介


有个小伙伴最近咨询我,前段时间他被面试官问了 synchronized 公平锁还是非公平锁?当时就蒙圈了,最后面试结果可想而知,今天我们就用一个通俗的案例加上代码来说明公平锁非公平锁。其实公平锁这个概念是 JUC 工具包才有的,比如 ReentrantLock 才有公平锁的概念,这篇文章我们结合生活中的实例用 2 段代码说明 ReentrantLock 公平锁和非公平锁,以及证明 synchronized 是非公平锁的。希望对小伙伴有帮助。

公平锁、非公平锁概念


  • 公平锁:举一个简单例子,有五个同学每天必须排队去打饭,为了简单起见,我们给这五名同学每人定义一个编号,分别为编号 001 编号 005,这五名同学按先来后到的排队,打饭,先来的同学能先打到饭。每个同学都是一个线程,在这个过程中后来的同学是不允许插队的,这就是公平锁

  • 非公平锁:后来到同学不一定后打到饭,在打饭的过程中,是允许插队的,这种线程插入的行为人们认为是不公平的。举个例子,比如编号为 001,002,003,004 的同学先到先排队了,005 最后来排队本应该排在 004 后面的,但是 005 看 001 正好打完饭离开,他就去插队了,也就是打饭的顺序由 001->002->003->004->005 变为 001->005->002->003->004。其实你现在应该理解了,公平锁就是正常排队,非公平就是插队。当然你可能会有疑问?是不是 005 插到 001 的后面一定会成功,答案是不一定,这要看时机的,我们刚才说了“005 看 001 正好打完饭离开”,下面应该是 002 了,可能打饭阿姨还没问 002 准备吃什么,就看 005 已经排到前面去了,那 005 就插队成功了,这就是时机。下面我们用程序代码来加深理解。


synchronized 非公平锁



/** * @author :jiaolian * @date :Created in 2020-12-31 16:01 * @description:食堂打饭:synchronized不公平 * @modified By: * 公众号:叫练 */public class SyncUnFairLockTest {
//食堂 private static class DiningRoom { //获取食物 public void getFood() { System.out.println(Thread.currentThread().getName()+":排队中"); synchronized (this) { System.out.println(Thread.currentThread().getName()+":@@@@@@打饭中@@@@@@@"); } } }
public static void main(String[] args) { DiningRoom diningRoom = new DiningRoom(); //让5个同学去打饭 for (int i=0; i<5; i++) { new Thread(()->{ diningRoom.getFood(); },"同学编号:00"+(i+1)).start(); } }}
复制代码

如上代码:我们定义一个内部类 DiningRoom 表示食堂,getFood 方法里面用 synchronized 锁修饰 this 指向 DiningRoom 的实例对象(22 行中的 diningRoom 对象),主类中让编号 001 至 005 五个同学同时去打饭,用于测试先排队的同学是否能先打到饭?运行程序得到其中一种执行结果如下图所示,002->004->001->003->005 同学先去排队,但打饭的顺序是 002->003->001->004->005,说明这里 003 和 001 两个同学插队了,插到 004 前面了,我们详细分析执行过程,002 先抢到锁打饭了,释放了锁,本来应该是接下来是 004 抢到锁去打饭(因为 004 是比 003 先来排队),但 003 抢到锁,打饭了,释放了锁,这是第一次插队。现在还是来 004 抢锁,但是没抢到又被 001 抢到了,释放锁后才被 004 抢到,这是第二次插队,后面分别再是 004->005 抢到锁,释放锁,程序执行完毕。因为 003 和 001 插队,我们用代码证明了 synchronized 是非公平锁。紧接着我们来看下 ReentrantLock 公平锁和非公平锁。


image.png


ReentrantLock 非公平锁



import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;
/** * @author :jiaolian * @date :Created in 2020-12-31 11:11 * @description:非公平锁测试 在获取锁的时候和再获取锁的顺序不一致; * @modified By: * 公众号:叫练 */public class UnFairLockTest {
private static final Lock LOCK = new ReentrantLock(false);
//食堂 private static class DiningRoom { //获取食物 public void getFood() { try { System.out.println(Thread.currentThread().getName()+":正在排队"); LOCK.lock(); System.out.println(Thread.currentThread().getName()+":@@@@@@打饭中@@@@@@@"); } catch (Exception e) { e.printStackTrace(); } finally { LOCK.unlock(); } } }
public static void main(String[] args) throws InterruptedException { DiningRoom diningRoom = new DiningRoom(); //让5个同学去打饭 for (int i=0; i<5; i++) { new Thread(()->{ diningRoom.getFood(); },"同学编号:00"+(i+1)).start(); } }}
复制代码

如上代码:我们在代码第 13 行中定义了 Lock LOCK = new ReentrantLock(false);ReentrantLock 的参数是 false 表示非公平锁,上面代码需要用 LOCK.lock()加锁,LOCK.unlock()解锁,需要放入 try,finally 代码块中,目的是如果 try 中加锁后代码发生异常锁最终执行 LOCK.unlock(),锁总能被释放。主类中让编号 001 至 005 五个同学同时去打饭,得到其中一种执行结果如下图所示,001->004->005->003->002 同学先去排队,但打饭的顺序是 001->005->004->003->002,这里 005 同学插队了,插到 004 前面。我们详细分析执行过程:001 先来抢到锁打饭了并释放了锁,接下来本应该是 004 抢到锁,因为它先排队,但 005 却在 004 之前抢到锁,打饭了,005 比 004 后来,却先打饭,这就是不公平锁,后面的执行结果按先来后到执行,程序结束。我们用代码证明了 ReentrantLock 是非公平的锁。紧接着我们来看下 ReentrantLock 另一种作为公平锁的情况。


image.png


ReentrantLock 公平锁


基于上面的案例,我们不重复贴代码了,将上述代码中 13 行的 private static final Lock LOCK = new ReentrantLock(false);参数由 false 改为 true,private static final Lock LOCK = new ReentrantLock(true);无论执行多少次可以得出一个结论:先排队的童鞋能先打饭,不允许插队体现的就是公平锁。


image.png


ReentrantLock 底层原理


ReentrantLock 是基于 AbstractQueuedSynchronizer(抽象队列同步器,简称 aqs)实现的,aqs 底层维护了一个带头的双向链表,用来同步线程,链表每个节点用 Node 表示,每个 Node 会记录线程信息,上下节点,节点状态等信息,aqs 控制 Node 的生命周期。如下图所示,aqs 也包含条件队列,锁和条件队列(condition)是一对多的关系,也就是说一个锁可以对应多个条件队列,线程间的通信在条件队列里通过 await,single/singleAll 方法控制,synchronized 只有一个条件队列用 wait,notify/notifyAll 来实现,这里不展开说了,《母鸡下蛋实例:多线程通信生产者和消费者 wait/notify 和 condition/await/signal 条件队列》和《Synchronized 用法原理和锁优化升级过程(面试)》可以看我文章,里面有大量清晰简单案例。条件队列也是以链表形式存在。Lock 是基于 juc 包实现,synchronized 是本地方法基于 c++实现。


image.png


总结


今天用生活中的例子转化成代码,详细的介绍了公平锁和非公平锁,并简单的介绍了 aqs 实现原理,给您的建议是认真把代码敲一遍,如果执行了一遍代码应该能看明白,喜欢的请点赞加关注哦。我是叫练【公众号】,边叫边练。


image.png


发布于: 2021 年 01 月 03 日阅读数: 33
用户头像

叫练

关注

我是叫练,边叫边练 2020.06.11 加入

Java高级工程师,熟悉多线程,JVM

评论

发布
暂无评论
排队打饭:公平锁和非公平锁(面试)