查看原文
其他

看完你就明白的锁系列之锁的公平性

cxuan Java建设者 2019-12-19

点击蓝色“Java建设者”关注我哟

加个“星标”,阅读我的文章,一起学习成长。



这是Java建设者的第 41 篇原创文章


这是看完你就明白的锁系列的第四篇文章

文章一览请看这里

看完你就应该能明白的悲观锁和乐观锁

看完你就明白的锁系列之自旋锁

看完你就明白的锁系列之锁的状态

此篇文章我们来探讨一下什么是锁的公平性

锁的公平性与非公平性

我们知道,在并发环境中,多个线程需要对同一资源进行访问,同一时刻只能有一个线程能够获取到锁并进行资源访问,那么剩下的这些线程怎么办呢?这就好比食堂排队打饭的模型,最先到达食堂的人拥有最先买饭的权利,那么剩下的人就需要在第一个人后面排队,这是理想的情况,即每个人都能够买上饭。那么现实情况是,在你排队的过程中,就有个别不老实的人想走捷径,插队打饭,如果插队的这个人后面没有人制止他这种行为,他就能够顺利买上饭,如果有人制止,他就也得去队伍后面排队。

对于正常排队的人来说,没有人插队,每个人都在等待排队打饭的机会,那么这种方式对每个人来说都是公平的,先来后到嘛。这种锁也叫做公平锁。


那么假如插队的这个人成功买上饭并且在买饭的过程不管有没有人制止他,他的这种行为对正常排队的人来说都是不公平的,这在锁的世界中也叫做非公平锁。


那么我们根据上面的描述可以得出下面的结论

公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的FIFO先进先出顺序。而非公平锁就是一种获取锁的抢占机制,是随机获得锁的,和公平锁不一样的就是先来的不一定先得到锁,这个方式可能造成某些线程一直拿不到锁,结果也就是不公平的了。

锁公平性的实现

在 Java 中,我们一般通过 ReetrantLock 来实现锁的公平性

我们分别通过两个例子来讲解一下锁的公平性和非公平性

锁的公平性

public class MyFairLock extends Thread{

private ReentrantLock lock = new ReentrantLock(true);
public void fairLock(){
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + "正在持有锁");
}finally {
System.out.println(Thread.currentThread().getName() + "释放了锁");
lock.unlock();
}
}

public static void main(String[] args) {
MyFairLock myFairLock = new MyFairLock();
Runnable runnable = () -> {
System.out.println(Thread.currentThread().getName() + "启动");
myFairLock.fairLock();
};
Thread[] thread = new Thread[10];
for(int i = 0;i < 10;i++){
thread[i] = new Thread(runnable);
}
for(int i = 0;i < 10;i++){
thread[i].start();
}
}
}

我们创建了一个 ReetrantLock,并给构造函数传了一个 true,我们可以查看 ReetrantLock 的构造函数

public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

根据 JavaDoc 的注释可知,如果是 true 的话,那么就会创建一个 ReentrantLock 的公平锁,然后并创建一个 FairSync ,FairSync 其实是一个 Sync 的内部类,它的主要作用是同步对象以获取公平锁。

而 Sync 是 ReentrantLock 中的内部类,Sync 继承 AbstractQueuedSynchronizer 类,AbstractQueuedSynchronizer 就是我们常说的 AQS ,它是 JUC(java.util.concurrent) 中最重要的一个类,通过它来实现独占锁和共享锁。

abstract static class Sync extends AbstractQueuedSynchronizer {...}

也就是说,我们把 fair 参数设置为 true 之后,就可以实现一个公平锁了,是这样吗?我们回到示例代码,我们可以执行一下这段代码,它的输出是顺序获取的(碍于篇幅的原因,这里就暂不贴出了),也就是说我们创建了一个公平锁

锁的非公平性

与公平性相对的就是非公平性,我们通过设置 fair 参数为 true,便实现了一个公平锁,与之相对的,我们把 fair 参数设置为 false,是不是就是非公平锁了?用事实证明一下

private ReentrantLock lock = new ReentrantLock(false);

其他代码不变,我们执行一下看看输出(部分输出)

Thread-1启动
Thread-4启动
Thread-1正在持有锁
Thread-1释放了锁
Thread-5启动
Thread-6启动
Thread-3启动
Thread-7启动
Thread-2启动

可以看到,线程的启动并没有按顺序获取,可以看出非公平锁对锁的获取是乱序的,即有一个抢占锁的过程。也就是说,我们把 fair 参数设置为 false 便实现了一个非公平锁。

公平锁的原理

接下来,我们通过 ReentrantLock 源码来讲解公平锁和非公平锁。首先先来认识一下

ReentrantLock 是什么

ReentrantLock 基本概述

ReentrantLock 是一把可重入锁,也是一把互斥锁,它具有与 synchronized 相同的方法和监视器锁的语义,但是它比 synchronized 有更多可扩展的功能。

ReentrantLock 的可重入性是指它可以由上次成功锁定但还未解锁的线程拥有。当只有一个线程尝试加锁时,该线程调用 lock() 方法会立刻返回成功并直接获取锁。如果当前线程已经拥有这把锁,这个方法会立刻返回。可以使用 isHeldByCurrentThreadgetHoldCount 进行检查。

这个类的构造函数接受可选择的 fairness 参数,当 fairness 设置为 true 时,在多线程争夺尝试加锁时,锁倾向于对等待时间最长的线程访问,这也是公平性的一种体现。否则,锁不能保证每个线程的访问顺序,也就是非公平锁。与使用默认设置的程序相比,使用许多线程访问的公平锁的程序可能会显示较低的总体吞吐量(即较慢;通常要慢得多)。但是获取锁并保证线程不会饥饿的次数比较小。无论如何请注意:锁的公平性不能保证线程调度的公平性。因此,使用公平锁的多线程之一可能会连续多次获得它,而其他活动线程没有进行且当前未持有该锁。这也是互斥性 的一种体现。

也要注意的 tryLock() 方法不支持公平性。如果锁是可以获取的,那么即使其他线程等待,它仍然能够返回成功。

推荐使用下面的代码来进行加锁和解锁

class MyFairLock {
private final ReentrantLock lock = new ReentrantLock();

public void m() {
lock.lock();
try {
// ...
} finally {
lock.unlock()
}
}
}

ReentrantLock 锁通过同一线程最多支持2147483647个递归锁。尝试超过此限制会导致锁定方法引发错误。

ReentrantLock 如何实现锁公平性

我们在上面的简述中提到,ReentrantLock 是可以实现锁的公平性的,那么原理是什么呢?下面我们通过其源码来了解一下 ReentrantLock 是如何实现锁的公平性的

跟踪其源码发现,调用 Lock.lock() 方法其实是调用了 sync 的内部的方法

abstract void lock();

而 sync 是最基础的同步控制 Lock 的类,它有公平锁和非公平锁的实现。它继承 AbstractQueuedSynchronizer 即 使用 AQS 状态代表锁持有的数量。

lock 是抽象方法是需要被子类实现的,而继承了 AQS 的类主要有


我们可以看到,所有实现了 AQS 的类都位于 JUC 包下,主要有五类:ReentrantLockReentrantReadWriteLockSemaphoreCountDownLatchThreadPoolExecutor,其中 ReentrantLock、ReentrantReadWriteLock、Semaphore 都可以实现公平锁和非公平锁。

下面是公平锁 FairSync 的继承关系

非公平锁的NonFairSync 的继承关系

由继承图可以看到,两个类的继承关系都是相同的,我们从源码发现,公平锁和非公平锁的实现就是下面这段代码的区别(下一篇文章我们会从原理角度分析一下公平锁和非公平锁的实现)

通过上图中的源代码对比,我们可以明显的看出公平锁与非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors()

hasQueuedPredecessors() 也是 AQS 中的方法,它主要是用来 查询是否有任何线程在等待获取锁的时间比当前线程长,也就是说每个等待线程都是在一个队列中,此方法就是判断队列中在当前线程获取锁时,是否有等待锁时间比自己还长的队列,如果当前线程之前有排队的线程,返回 true,如果当前线程位于队列的开头或队列为空,返回 false。

综上,公平锁就是通过同步队列来实现多个线程按照申请锁的顺序来获取锁,从而实现公平的特性。非公平锁加锁时不考虑排队等待问题,直接尝试获取锁,所以存在后申请却先获得锁的情况。

文章参考:

https://tech.meituan.com/2018/11/15/java-lock.html

https://www.jianshu.com/p/eaea337c5e5b

https://blog.csdn.net/oChangWen/article/details/77622889

ReentrantLock(重入锁)功能详解和应用演示


系列文章

程序员需要了解的硬核知识之CPU


程序员需要了解的硬核知识之内存


关于二进制世界的秘密


看完你就明白的锁系列之锁的状态

看完你就明白的锁系列之自旋锁


看完你就应该能明白的悲观锁和乐观锁


带你涨姿势的认识一下kafka


与我时刻保持联系是对我最大的鼓励


    

你点的每个好看,我都认真当成了喜欢


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存