这是我2021年的第7篇原创文章,原汁原味的技术之路尽在Jerrycodes


写这个文章的时候让我想起了让子弹飞的一个台词

公平,公平,还是tmd公平!

  • 什么是公平和非公平

  • 公平的场景

  • 不公平的场景

  • 对比公平和非公平的优缺点

    • 源码分析

  • 总结

什么是公平和非公平

首先,我们来看下什么是公平锁和非公平锁。

公平锁指的是按照线程请求的顺序,来分配锁;

非公平锁指的是不完全按照请求的顺序,在一定情况下,可以允许插队。但需要注意这里的非公平并不是指完全的随机,不是说线程可以任意插队,而是仅仅“在合适的时机”插队。

那么什么时候是合适的时机呢?

假设当前线程在请求获取锁的时候,恰巧前一个持有锁的线程释放了这把锁,那么当前申请锁的线程就可以不顾已经等待的线程而选择立刻插队。但是如果当前线程请求的时候,前一个线程并没有在那一时刻释放锁,那么当前线程还是一样会进入等待队列。

为了能够更好的理解公平锁和非公平锁,我们举一个生活中的例子,假设我们还在学校读书,去食堂排队买饭,我排在队列的第二个,我前面还有一位同学,但此时我脑子里想的不是午饭,而是上午的一道数学题并陷入深思,所以当前面的同学打完饭之后轮到我时我走神了,并也没注意到现在轮到我了,此时前面的同学突然又回来插队,说“不好意思,阿姨麻烦给我加个鸡腿”,像这样的行为就可以类比我们的公平锁和非公平锁。

让我们考虑一种情况,假设线程 A 持有一把锁,线程 B 请求这把锁,由于线程 A 已经持有这把锁了,所以线程 B 会陷入等待,在等待的时候线程 B 会被挂起,也就是进入阻塞状态,那么当线程 A 释放锁的时候,本该轮到线程 B 苏醒获取锁,但如果此时突然有一个线程 C 插队请求这把锁。

那么根据非公平的策略,会把这把锁给线程 C,这是因为唤醒线程 B 是需要很大开销的,很有可能在唤醒之前,线程 C 已经拿到了这把锁并且执行完任务释放了这把锁。

相比于等待唤醒线程 B 的漫长过程,插队的行为会让线程 C 本身跳过陷入阻塞的过程,如果在锁代码中执行的内容不多的话,线程 C 就可以很快完成任务,并且在线程 B 被完全唤醒之前,就把这个锁交出去,这样是一个双赢的局面,对于线程 C 而言,不需要等待提高了它的效率,而对于线程 B 而言,它获得锁的时间并没有推迟,因为等它被唤醒的时候,线程 C 早就释放锁了,因为线程 C 的执行速度相比于线程 B 的唤醒速度,是很快的,所以 Java 设计者设计非公平锁,是为了提高整体的运行效率。

公平的场景

下面我们用图示来说明公平和非公平的场景,先来看公平的情况。假设我们创建了一个公平锁,此时有 4 个线程按顺序来请求公平锁,线程 1 在拿到这把锁之后,线程 2、3、4 会在等待队列中开始等待,然后等线程 1 释放锁之后,线程 2、3、4 会依次去获取这把锁,线程 2 先获取到的原因是它等待的时间最长。

不公平的场景

下面我们再来看看非公平的情况,假设线程 1 在解锁的时候,突然有线程 5 尝试获取这把锁,那么根据我们的非公平策略,线程 5 是可以拿到这把锁的,尽管它没有进入等待队列,而且线程 2、3、4 等待的时间都比线程 5 要长,但是从整体效率考虑,这把锁此时还是会交给线程 5 持有。

下面我们来用代码演示看下公平和非公平的实际效果,代码如下:

/*** 描述:演示公平锁,分别展示公平和不公平的情况,非公平锁会让现在持有锁的线程优先再次获取到锁。代码借鉴自Java并发编程实战手册2.7。*/
public class FairAndUnfair {public static void main(String args[]) {PrintQueue printQueue = new PrintQueue();Thread thread[] = new Thread[10];for (int i = 0; i < 10; i++) {thread[i] = new Thread(new Job(printQueue), "Thread " + i);}for (int i = 0; i < 10; i++) {thread[i].start();try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}}}class Job implements Runnable {private PrintQueue printQueue;public Job(PrintQueue printQueue) {this.printQueue = printQueue;}@Overridepublic void run() {System.out.printf("%s: Going to print a job\n", Thread.currentThread().getName());printQueue.printJob(new Object());System.out.printf("%s: The document has been printed\n", Thread.currentThread().getName());}}class PrintQueue {private final Lock queueLock = new ReentrantLock(false);public void printJob(Object document) {queueLock.lock();try {Long duration = (long) (Math.random() * 10000);System.out.printf("%s: PrintQueue: Printing a Job during %d seconds\n",Thread.currentThread().getName(), (duration / 1000));Thread.sleep(duration);} catch (InterruptedException e) {e.printStackTrace();} finally {queueLock.unlock();}queueLock.lock();try {Long duration = (long) (Math.random() * 10000);System.out.printf("%s: PrintQueue: Printing a Job during %d seconds\n",Thread.currentThread().getName(), (duration / 1000));Thread.sleep(duration);} catch (InterruptedException e) {e.printStackTrace();} finally {queueLock.unlock();}}
}

我们可以通过改变 new ReentrantLock(false) 中的参数来设置公平/非公平锁。以上代码在公平的情况下的输出:

Thread 0: Going to print a job
Thread 0: PrintQueue: Printing a Job during 5 seconds
Thread 1: Going to print a job
Thread 2: Going to print a job
Thread 3: Going to print a job
Thread 4: Going to print a job
Thread 5: Going to print a job
Thread 6: Going to print a job
Thread 7: Going to print a job
Thread 8: Going to print a job
Thread 9: Going to print a job
Thread 1: PrintQueue: Printing a Job during 3 seconds
Thread 2: PrintQueue: Printing a Job during 4 seconds
Thread 3: PrintQueue: Printing a Job during 3 seconds
Thread 4: PrintQueue: Printing a Job during 9 seconds
Thread 5: PrintQueue: Printing a Job during 5 seconds
Thread 6: PrintQueue: Printing a Job during 7 seconds
Thread 7: PrintQueue: Printing a Job during 3 seconds
Thread 8: PrintQueue: Printing a Job during 9 seconds
Thread 9: PrintQueue: Printing a Job during 5 seconds
Thread 0: PrintQueue: Printing a Job during 8 seconds
Thread 0: The document has been printed
Thread 1: PrintQueue: Printing a Job during 1 seconds
Thread 1: The document has been printed
Thread 2: PrintQueue: Printing a Job during 8 seconds
Thread 2: The document has been printed
Thread 3: PrintQueue: Printing a Job during 2 seconds
Thread 3: The document has been printed
Thread 4: PrintQueue: Printing a Job during 0 seconds
Thread 4: The document has been printed
Thread 5: PrintQueue: Printing a Job during 7 seconds
Thread 5: The document has been printed
Thread 6: PrintQueue: Printing a Job during 3 seconds
Thread 6: The document has been printed
Thread 7: PrintQueue: Printing a Job during 9 seconds
Thread 7: The document has been printed
Thread 8: PrintQueue: Printing a Job during 5 seconds
Thread 8: The document has been printed
Thread 9: PrintQueue: Printing a Job during 9 seconds
Thread 9: The document has been printed

可以看出,线程直接获取锁的顺序是完全公平的,先到先得。

而以上代码在非公平的情况下的输出是这样的:

Thread 0: Going to print a job
Thread 0: PrintQueue: Printing a Job during 6 seconds
Thread 1: Going to print a job
Thread 2: Going to print a job
Thread 3: Going to print a job
Thread 4: Going to print a job
Thread 5: Going to print a job
Thread 6: Going to print a job
Thread 7: Going to print a job
Thread 8: Going to print a job
Thread 9: Going to print a job
Thread 0: PrintQueue: Printing a Job during 8 seconds
Thread 0: The document has been printed
Thread 1: PrintQueue: Printing a Job during 9 seconds
Thread 1: PrintQueue: Printing a Job during 8 seconds
Thread 1: The document has been printed
Thread 2: PrintQueue: Printing a Job during 6 seconds
Thread 2: PrintQueue: Printing a Job during 4 seconds
Thread 2: The document has been printed
Thread 3: PrintQueue: Printing a Job during 9 seconds
Thread 3: PrintQueue: Printing a Job during 8 seconds
Thread 3: The document has been printed
Thread 4: PrintQueue: Printing a Job during 4 seconds
Thread 4: PrintQueue: Printing a Job during 2 seconds
Thread 4: The document has been printed
Thread 5: PrintQueue: Printing a Job during 2 seconds
Thread 5: PrintQueue: Printing a Job during 5 seconds
Thread 5: The document has been printed
Thread 6: PrintQueue: Printing a Job during 2 seconds
Thread 6: PrintQueue: Printing a Job during 6 seconds
Thread 6: The document has been printed
Thread 7: PrintQueue: Printing a Job during 6 seconds
Thread 7: PrintQueue: Printing a Job during 4 seconds
Thread 7: The document has been printed
Thread 8: PrintQueue: Printing a Job during 3 seconds
Thread 8: PrintQueue: Printing a Job during 6 seconds
Thread 8: The document has been printed
Thread 9: PrintQueue: Printing a Job during 3 seconds
Thread 9: PrintQueue: Printing a Job during 5 seconds
Thread 9: The document has been printed

可以看出,非公平情况下,存在抢锁“插队”的现象,比如Thread 0 在释放锁后又能优先获取到锁,虽然此时在等待队列中已经有 Thread 1 ~ Thread 9 在排队了。

对比公平和非公平的优缺点

我们接下来对比公平和非公平的优缺点,如表格所示。

公平锁的优点在于各个线程公平平等,每个线程等待一段时间后,都有执行的机会,而它的缺点就在于整体执行速度更慢,吞吐量更小,相反非公平锁的优势就在于整体执行速度更快,吞吐量更大,但同时也可能产生线程饥饿问题,也就是说如果一直有线程插队,那么在等待队列中的线程可能长时间得不到运行。

源码分析

下面我们来分析公平和非公平锁的源码,具体看下它们是怎样实现的,可以看到在 ReentrantLock 类包含一个 Sync 类,这个类继承自AQS(AbstractQueuedSynchronizer),代码如下:


public class ReentrantLock implements Lock, java.io.Serializable {private static final long serialVersionUID = 7373984872572414699L;/** Synchronizer providing all implementation mechanics */private final Sync sync;
Sync 类的代码:
abstract static class Sync extends AbstractQueuedSynchronizer {...}

根据代码可知,Sync 有公平锁 FairSync 和非公平锁 NonfairSync两个子类:

static final class NonfairSync extends Sync {...}
static final class FairSync extends Sync {...}

下面我们来看一下公平锁与非公平锁的加锁方法的源码。

公平锁的锁获取源码如下:

protected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {if (!hasQueuedPredecessors() && //这里判断了 hasQueuedPredecessors()compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}} else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0) {throw new Error("Maximum lock count exceeded");}setState(nextc);return true;}return false;
}

非公平锁的锁获取源码如下:


final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {if (compareAndSetState(0, acquires)) { //这里没有判断      hasQueuedPredecessors()setExclusiveOwnerThread(current);return true;}}else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;
}

通过对比,我们可以明显的看出公平锁与非公平锁的 lock() 方法唯一的区别就在于公平锁在获取锁时多了一个限制条件:hasQueuedPredecessors() 为 false,这个方法就是判断在等待队列中是否已经有线程在排队了。

这也就是公平锁和非公平锁的核心区别

如果是公平锁,那么一旦已经有线程在排队了,当前线程就不再尝试获取锁;

对于非公平锁而言,无论是否已经有线程在排队,都会尝试获取一下锁,获取不到的话,再去排队。

这里有一个特例需要我们注意,针对  tryLock() 方法,它不遵守设定的公平原则。

例如,当有线程执行 tryLock() 方法的时候,一旦有线程释放了锁,那么这个正在 tryLock 的线程就能获取到锁,即使设置的是公平锁模式,即使在它之前已经有其他正在等待队列中等待的线程,简单地说就是 tryLock 可以插队。

看它的源码就会发现:


public boolean tryLock() {return sync.nonfairTryAcquire(1);
}

这里调用的就是 nonfairTryAcquire(),表明了是不公平的,和锁本身是否是公平锁无关。

综上所述,公平锁就是会按照多个线程申请锁的顺序来获取锁,从而实现公平的特性。非公平锁加锁时不考虑排队等待情况,直接尝试获取锁,所以存在后申请却先获得锁的情况,但由此也提高了整体的效率。

总结

非公平锁的设计初衷是为了提高性能,又get到一个知识点。

Jerry哥建立了一个优质的技术微信群,主要用于2022秋招/实习交流群,正在准备2021春招的人也可以加入。群内嘉宾有前美团技术人索隆,微软工程师C哥和已经成功上岸的J哥。欢迎大家扫码加我,备注学校+姓名+岗位,我会拉大家进群。

既然有公平锁,为什么还要有非公平锁相关推荐

  1. 24-讲一讲公平锁和非公平锁,为什么要“非公平”?

    什么是公平和非公平 首先,我们来看下什么是公平锁和非公平锁,公平锁指的是按照线程请求的顺序,来分配锁:而非公平锁指的是不完全按照请求的顺序,在一定情况下,可以允许插队.但需要注意这里的非公平并不是指完 ...

  2. Java多线程学习十五:公平锁和非公平锁,为什么要“非公平”?

    什么是公平和非公平 公平锁 指的是按照线程请求的顺序,来分配锁: 非公平锁 指的是不完全按照请求的顺序,在一定情况下,可以允许插队.但需要注意这里的非公平并不是指完全的随机,不是说线程可以任意插队,而 ...

  3. java公平索非公平锁_java中的非公平锁不怕有的线程一直得不到执行吗

    首先来看公平锁和非公平锁,我们默认使用的锁是非公平锁,只有当我们显示设置为公平锁的情况下,才会使用公平锁,下面我们简单看一下公平锁的源码,如果等待队列中没有节点在等待,则占有锁,如果已经存在等待节点, ...

  4. 公平锁和非公平锁介绍

    公平锁: 解释:多个线程按照申请锁的顺序执行,先来后到. 优点:所有的线程都能得到资源,不会饿死在队列中. 缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销 ...

  5. java锁(公平锁和非公平锁、可重入锁(又名递归锁)、自旋锁、独占锁(写)/共享锁(读)/互斥锁、读写锁)

    前言 本文对Java的一些锁的概念和实现做个整理,涉及:公平锁和非公平锁.可重入锁(又名递归锁).自旋锁.独占锁(写)/共享锁(读)/互斥锁.读写锁 公平锁和非公平锁 概念 公平锁是指多个线程按照申请 ...

  6. java公平所与非公平所_一张图读懂Java非公平锁与公平锁

    前言 文本已收录至我的GitHub仓库,欢迎Star:github.com/bin39232820- 种一棵树最好的时间是十年前,其次是现在 我知道很多人不玩qq了,但是怀旧一下,欢迎加入六脉神剑Ja ...

  7. java投票锁_Java并发编程锁之独占公平锁与非公平锁比较

    Java并发编程锁之独占公平锁与非公平锁比较 公平锁和非公平锁理解: 在上一篇文章中,我们知道了非公平锁.其实Java中还存在着公平锁呢.公平二字怎么理解呢?和我们现实理解是一样的.大家去排队本着先来 ...

  8. java 对变量加锁_Java最全锁剖析:独享锁/共享锁+公平锁/非公平锁+乐观锁/悲观锁...

    乐观锁 VS 悲观锁 乐观锁与悲观锁是一种广义上的概念,体现了看待线程同步的不同角度,在Java和数据库中都有此概念对应的实际应用. 1.乐观锁 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会 ...

  9. java 共享锁 独占锁_Java并发编程锁之独占公平锁与非公平锁比较

    Java并发编程锁之独占公平锁与非公平锁比较 公平锁和非公平锁理解: 在上一篇文章中,我们知道了非公平锁.其实Java中还存在着公平锁呢.公平二字怎么理解呢?和我们现实理解是一样的.大家取排队本着先来 ...

最新文章

  1. JSONP的实现原理
  2. 2015 HIAST Collegiate Programming Contest J
  3. Windows Server 2008 使用WDS自动部署操作系统
  4. 配置System Center Data Protection Manager 2007
  5. 跟锦数学190314
  6. 软件开发中 前台、中台、后台英文_最近处处惹人爱的中台到底是什么
  7. c++中的运算符异或^,与,或|
  8. 五万以内买什么车合适?
  9. 学习《21天学通java(第7版)》之错误收集
  10. cad相对坐标快捷键_cad相对坐标怎么输入?CAD中输入西安80坐标绘图的教程
  11. vue脚手架安装步骤详情
  12. Android仿微信朋友圈发布动态
  13. 华钜同创:拯救你的亚马逊销量!你需要知道这些技巧
  14. Cloud一分钟 |互联网之冬;华为停招,BAT裁员;苹果下线拼多多应用;意媒谈DG风波:中国人记性差...
  15. 店外营销吸睛,店内体验升级丨餐饮品牌如何「吃」透数据?
  16. 创新实训(9)——SpringBoot整合solr
  17. 记一次WebService调用踩的坑
  18. 限制服务器访问ip(或端口)
  19. F007-正本清源说奥派 #F1170
  20. 下载lpv9_v9社区APP下载-v9社区APP官方版 v1.0.4-114手机乐园

热门文章

  1. HT1382时钟芯片代码
  2. 关于固定表头的一种方式
  3. CCF 推荐中文科技期刊目录
  4. IntelliJ IDEA的简介
  5. 意甲-米兰3-0胜 卡卡两球一助攻达百球里程碑
  6. 网优谷|C++ 语言好学吗?
  7. PyTorch:优化神经网络训练的17种方法
  8. 进制转换和位操作(详细,通俗,易懂)
  9. easyexcel使用和遇到的问题点
  10. 酒店预订系统java_JavaWeb酒店预订系统