zoukankan      html  css  js  c++  java
  • 排队打饭:公平锁和非公平锁(面试)

    简介


    有个小伙伴最近咨询我,前段时间他被面试官问了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

  • 相关阅读:
    《链队列---队列的链式表示和实现》
    《栈的应用_版本1.2(实现了可以在一次运行后进行多次操作)》
    《栈的应用_版本1.1(实现了如何十进制转十六进制)》
    《栈的应用 版本1.0》
    《栈的基本操作》
    《单链表练习》
    hdu5887 Herbs Gathering
    CF198 div1 D
    hdu5893 List wants to travel
    hdu5556 Land of Farms
  • 原文地址:https://www.cnblogs.com/jiaolian/p/14226957.html
Copyright © 2011-2022 走看看