• 面试突击46:公平锁和非公平锁有什么区别?


    从公平的角度来说,Java 中的锁总共可分为两类:公平锁和非公平锁。但公平锁和非公平锁有哪些区别?孰优孰劣呢?在 Java 中的应用场景又有哪些呢?接下来我们一起来看。

    正文

    公平锁:每个线程获取锁的顺序是按照线程访问锁的先后顺序获取的,最前面的线程总是最先获取到锁。
    非公平锁:每个线程获取锁的顺序是随机的,并不会遵循先来先得的规则,所有线程会竞争获取锁。
    举个例子,公平锁就像开车经过收费站一样,所有的车都会排队等待通过,先来的车先通过,如下图所示:
    image.png
    通过收费站的顺序也是先来先到,分别是张三、李四、王五,这种情况就是公平锁。
    而非公平锁相当于,来了一个强行加塞的老司机,它不会准守排队规则,来了之后就会试图强行加塞,如果加塞成功就顺利通过,当然也有可能加塞失败,如果失败就乖乖去后面排队,这种情况就是非公平锁。
    image.png

    应用场景

    在 Java 语言中,锁 synchronized 和 ReentrantLock 默认都是非公平锁,当然我们在创建 ReentrantLock 时,可以手动指定其为公平锁,但 synchronized 只能为非公平锁。
    ReentrantLock 默认为非公平锁可以在它的源码实现中得到验证,如下源码所示:

    当使用 new ReentrantLock(true) 时,可以创建公平锁,如下源码所示:
    image.png

    公平和非公平锁代码演示

    接下来我们使用 ReentrantLock 来演示一下公平锁和非公平锁的执行差异,首先定义一个公平锁,开启 3 个线程,每个线程执行两次加锁和释放锁并打印线程名的操作,如下代码所示:

    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class ReentrantLockFairTest {
        static Lock lock = new ReentrantLock(true);
        public static void main(String[] args) throws InterruptedException {
            for (int i = 0; i < 3; i++) {
                new Thread(() -> {
                    for (int j = 0; j < 2; j++) {
                        lock.lock();
                        System.out.println("当前线程:" + Thread.currentThread()
                                .getName());
                        lock.unlock();
                    }
                }).start();
            }
        }
    }
    

    以上程序的执行结果如下图所示:
    image.png
    接下来我们使用非公平锁来执行上面的代码,具体实现如下:

    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class ReentrantLockFairTest {
        static Lock lock = new ReentrantLock();
        public static void main(String[] args) throws InterruptedException {
            for (int i = 0; i < 3; i++) {
                new Thread(() -> {
                    for (int j = 0; j < 2; j++) {
                        lock.lock();
                        System.out.println("当前线程:" + Thread.currentThread()
                                .getName());
                        lock.unlock();
                    }
                }).start();
            }
        }
    }
    

    以上程序的执行结果如下图所示:
    image.png
    从上述结果可以看出,使用公平锁线程获取锁的顺序是:A -> B -> C -> A -> B -> C,也就是按顺序获取锁。而非公平锁,获取锁的顺序是 A -> A -> B -> B -> C -> C,原因是所有线程都争抢锁时,因为当前执行线程处于活跃状态,其他线程属于等待状态(还需要被唤醒),所以当前线程总是会先获取到锁,所以最终获取锁的顺序是:A -> A -> B -> B -> C -> C。

    执行流程分析

    公平锁执行流程

    获取锁时,先将线程自己添加到等待队列的队尾并休眠,当某线程用完锁之后,会去唤醒等待队列中队首的线程尝试去获取锁,锁的使用顺序也就是队列中的先后顺序,在整个过程中,线程会从运行状态切换到休眠状态,再从休眠状态恢复成运行状态,但线程每次休眠和恢复都需要从用户态转换成内核态,而这个状态的转换是比较慢的,所以公平锁的执行速度会比较慢。

    非公平锁执行流程

    当线程获取锁时,会先通过 CAS 尝试获取锁,如果获取成功就直接拥有锁,如果获取锁失败才会进入等待队列,等待下次尝试获取锁。这样做的好处是,获取锁不用遵循先到先得的规则,从而避免了线程休眠和恢复的操作,这样就加速了程序的执行效率。
    公平锁和非公平锁的性能测试结果如下,以下测试数据来自于《Java并发编程实战》:

    image.png
    从上述结果可以看出,使用非公平锁的吞吐率(单位时间内成功获取锁的平均速率)要比公平锁高很多。

    优缺点分析

    公平锁的优点是按序平均分配锁资源,不会出现线程饿死的情况,它的缺点是按序唤醒线程的开销大,执行性能不高。
    非公平锁的优点是执行效率高,谁先获取到锁,锁就属于谁,不会“按资排辈”以及顺序唤醒,但缺点是资源分配随机性强,可能会出现线程饿死的情况。

    总结

    在 Java 语言中,锁的默认实现都是非公平锁,原因是非公平锁的效率更高,使用 ReentrantLock 可以手动指定其为公平锁。非公平锁注重的是性能,而公平锁注重的是锁资源的平均分配,所以我们要选择合适的场景来应用二者。

    是非审之于己,毁誉听之于人,得失安之于数。

    公众号:Java面试真题解析

    面试合集:https://gitee.com/mydb/interview

  • 相关阅读:
    FFplay文档解读-18-音频过滤器三
    cgroup version jdk version k8s
    Linux命令200例:free用来显示系统内存使用情况
    AtCoder abc 133
    【Prometheus】监控Kubernetes
    互联网Java工程师面试题·Java 总结篇·第一弹
    writer.writerows()和writer.writerow
    Alibaba新产SpringBoot深度历险(嵩山版)
    什么,有狗快跑!慢着,这次手把手教你怎么过安全狗!(sql注入篇)
    数据创新加速产业发展∣企企通亮相2023浙北CIO峰会,以技术驱动数智升级
  • 原文地址:https://www.cnblogs.com/vipstone/p/16248006.html