前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >啥?小胖连公平锁 & 非公平锁都不知道?真的菜!

啥?小胖连公平锁 & 非公平锁都不知道?真的菜!

作者头像
JavaFish
发布2020-12-17 14:31:19
4511
发布2020-12-17 14:31:19
举报
文章被收录于专栏:狗哥的 Java 世界

公平锁 & 非公平锁

来到多线程的第十二篇,前十一篇请点文末底部的上、下一篇标签,这篇说说什么是公平锁 & 非公平锁?开篇之前,先聊聊它们的定义以及优缺点。

「公平锁」:多个线程按顺序排队获取锁,每个线程获取机会均等,但永远只有队列首位线程能获取到锁。

  • 优点:每个线程等待一段时间后,都有执行的机会,不至于出现某个线程饿死在队列中。
  • 缺点:是队列里面除了第一个线程,其他的线程都会阻塞,cpu 唤醒阻塞线程的开销会很大。

「非公平锁」:多个线程(不管是不是队列首位)去获取锁时会尝试直接获取锁,能获取到就执行任务,否则乖乖排队。

  • 优点:获取锁更加灵活、吞吐量大、减少 CPU 唤醒线程的开销。
  • 缺点:会出现某些线程永远获取不到锁,饿死在队列中,最终由 JVM 回收。

说了这么一堆,大家也可能看得不是很明白。狗哥就举个在大学跟室友一起排队买早饭的场景。是这样的,帅得一批的狗哥(也就是我啦),跟室友小钊(渣男)、小宝以及小民去买早饭。狗哥以及室友们都看做是一个线程。

「首先是公平锁」

某天狗哥起的最晚,到了饭堂。室友们都已经在排队买早饭了,作为良好市民的狗哥自然也是乖乖过去排队。这样大家都准守秩序,先到先得,很公平。

公平锁

「然后是非公平锁」

还是狗哥起的最晚,到了食堂刚好小宝买完去上徐国保老师的课了。小钊这比还在思考今晚帮哪个妹子修电脑。狗哥不讲武德,趁机插了上去队头,见到是我插队,后面的小钊、小民即使不爽也不敢说啥,只能看着我买早餐。这就是非公平锁插队成功的例子。

非公平锁插队成功

但是,偶尔也有不成功的时候。比如小钊这比思考完要帮妹子阿毛修电脑,这时我想插上去被他发现使出一记闪电五连鞭把我给赶走了,狗哥也是欺软怕硬,只能乖乖去后面排队。这就是非公平锁插队失败的例子。

非公平锁插队失败

看完了这个例子是不是对非公平锁 & 公平锁这对 CP 的理解清晰了很多?刚看例子,不讲代码的行为无耻至极。下面随狗哥来读读源码。

源码分析

「如何使用?」

代码语言:javascript
复制
ReentrantLock lock = new ReentrantLock();

上面的代码熟悉么?其实,大家平时应该有使用过 ReentrantLock 的话就已经使用过非公平锁(ReentrantLock 默认)了。源码如下所示:

想要使用公平锁,创建锁的时候直接给个 true 即可。

代码语言:javascript
复制
ReentrantLock lock = new ReentrantLock(true);

「具体是怎么实现的?」

源码可以看到 ReentrantLock 内部有一个 Sync 内部类。他继承了 AbstractQueuedSynchronizer (也就是我们常说的 AQS),在操作锁的时候大多都是通过 Sync 实现的。

代码语言:javascript
复制
public class ReentrantLock implements Lock, java.io.Serializable {

        private static final long serialVersionUID = 7373984872572414699 L;

        /** Synchronizer providing all implementation mechanics */

        private final Sync sync;
代码语言:javascript
复制
abstract static class Sync extends AbstractQueuedSynchronizer {
    ...
}

下图得知,Sync 他又有两个子类,分别是 NonfairSync(非公平锁) & FairSync(公平锁),见名知义。

「非公平锁加锁实现」

从 nonfairTryAcquire 方法内部的 compareAndSetState 方法可以看到,非公平锁获取锁主要是通过 CAS 方式,修改 state 状态为 1,并且通过 setExclusiveOwnerThread (current) 把当前持有锁的线程改为自己。

代码语言:javascript
复制
static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691 L;

    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }

    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    // 获取锁的关键代码
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    } else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

从上面我们知道 AbstractQueuedSynchronizer (AQS) 是 ReentrantLock 加解锁的核心,这样说可能有些笼统,还是刚刚的场景,狗哥给你们画几个图理解下。

1、首先狗哥因为长得帅,线程狗哥一进来就 CAS 判断 state == 0 有戏,把自己设置为加锁线程,成功获取到锁并买到早饭了。

非公平锁狗哥顺利获取

2、这时小钊这逼过来想插队,但一判断 state 发现是 1,有人已经持有锁了。于是只能灰溜溜的滚回去排队了。

非公平锁渣男小钊获取失败

3、狗哥买完早饭,把 state 设置为 0 并释放锁,唤醒小钊。

非公平锁唤醒小钊

4、就在唤醒期间(小钊还没醒的时刻)烫了个原谅绿藻头的小宝不讲武德插队,先行 CAS 判断 state == 0,可以设置自己为加锁线程。这时小钊可算醒了,CAS 判断 state,发现是 1,这尼玛。狗子不是说到我了吗?怎么又被人占用了,于是又灰溜溜的去排队。

非公平锁小宝插队

以上就是非公平锁的加锁过程,上面提到非公平锁有部分线程可能会饿死,看完大家也可能理解了。小钊就是那个饿死的线程。。。

「公平锁加锁实现」

公平锁跟非公平锁加锁的逻辑差不多,唯一就是公平加锁的 if 判断中多了 hasQueuedPredecessors 是否队首的判断。

代码语言:javascript
复制
static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540 L;

    final void lock() {
        acquire(1);
    }

    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            // 多了 hasQueuedPredecessors 是否队首判断
            if (!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;
    }
}

这点从以下源码可以看出,就算看不懂。注释也写得很清楚。

hasQueuedPredecessors

以上还是刚刚的场景,狗哥继续给你们画几个图理解下公平锁。

1、狗哥今天起得比较早(人长得帅,还这么努力)。来到食堂就 CAS 判断 state 是不是 = 0,是就修改为 1,,一然后发现自己居然排第一,最后把自己设置为加锁线程,成功买早饭。

公平锁狗哥成功获取锁

2、小钊这比昨晚帮妹子修电脑起得比较晚,来到饭堂先判断下 state 判断状态。发现 = 1,有人占用。只能灰溜溜的去排队。

公平锁小钊滚回去排队

3、过段时间,狗哥买完早饭,将 state 设置为 0,并且把持有锁线程设置为 null,然后去唤醒队首的小钊。

(在小钊还未醒的时刻)另一位绿藻头小民,昨晚看语气助词片看的比较晚。来了就判断 state == 0,想插队。但是公平锁规定必须队首获取锁,他发现自己不是队首,没法获取锁很尴尬。

公平锁小民插队不成功

4、终于,小钊醒了。判断 state == 0,修改为 1。此时不能忘记还要看看自己是不是队首。发现是,最后把持有锁线程修改为自己,开心的买到了早餐。

公平锁小钊成功获取锁

看到这里,相信大家也彻底理解了吧?公平锁的缺点就是必须队首线程获取锁。如上例子小民都 CAS 了一遍,但因为不是队首,还是得阻塞。增加了 CPU 负担。

一个特例

针对 tryLock () 方法,它不遵守设定的公平原则。

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

看它的源码就会发现:

代码语言:javascript
复制
public boolean tryLock() {
    return sync.nonfairTryAcquire(1);
}

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

测试代码

代码语言:javascript
复制
/**
 * 描述:演示公平锁,分别展示公平和不公平的情况,非公平锁会让现在持有锁的线程优先再次获取到锁。代码借鉴自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;
    }

    @Override
    public 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) 中的参数来设置公平 / 非公平锁。以上代码在公平的情况下的输出:

代码语言:javascript
复制
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

可以看出,线程直接获取锁的顺序是完全公平的,先到先得。而以上代码在非公平的情况下的输出是这样的:

代码语言:javascript
复制
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

巨人的肩膀

  • https://kaiwu.lagou.com/course/courseInfo.htm?courseId=16#/detail/pc?id=262
  • https://mp.weixin.qq.com/s/Si6XvLd5NxkTUAi_IUncFg

-END-

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-12-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 一个优秀的废人 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 公平锁 & 非公平锁
    • 源码分析
      • 一个特例
        • 测试代码
          • 巨人的肩膀
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档