公平锁与非公平锁的核心原理

举报
酸菜鱼. 发表于 2022/11/30 21:11:57 2022/11/30
【摘要】 Lock锁接口方法前面了解到了synchronized锁,也知道了synchronized锁是一种JVM提供内置锁,但synchronized有一些缺点:比如不支持响应中断,不支持超时,不支持以非阻塞的方式获取锁等。而今天的主角Lock锁,需要我们手动获取锁和释放锁,里面有很多方式来获取锁,比如以阻塞方式获取锁,在指定时间内获取锁,非阻塞模式下抢占锁等,其方法源码如下(位于package ...

Lock锁接口方法

前面了解到了synchronized锁,也知道了synchronized锁是一种JVM提供内置锁,但synchronized有一些缺点:比如不支持响应中断,不支持超时,不支持以非阻塞的方式获取锁等。而今天的主角Lock锁,需要我们手动获取锁和释放锁,里面有很多方式来获取锁,比如以阻塞方式获取锁,在指定时间内获取锁,非阻塞模式下抢占锁等,其方法源码如下(位于package java.util.concurrent.locks包下):

//Lock接口下的方法
public interface Lock {
    //阻塞式抢占锁,如果抢到锁,则向下执行程序;抢占失败线程阻塞,直到释放锁才会进行抢占锁
    void lock();
    //可中断模式抢占锁,线程调用此方法能够中断线程
    void lockInterruptibly() throws InterruptedException;
    //非阻塞式尝试获取锁,调用此方法线程不会阻塞,抢到锁返回true,失败返回false
    boolean tryLock();
    //在指定时间内尝试获取锁,在指定时间内抢到锁成功返回true,失败返回false
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    //释放锁,线程执行完程序后,调用此方法来释放锁资源
    void unlock();
    //条件队列
    Condition newCondition();
    }

公平锁简介

公平锁,顾名思义,所有线程获取锁都是公平的。在多线程并发情况下,线程争抢锁时,首先会检查等待队列中是否有其他线程在等待。如果等待队列为空,没有线程在等待,那么当前线程会拿到锁资源;如果等待队列中有其他线程在等待,那么当前线程会排到等待队列的尾部,好比我们排队买东西一样。

ReentranLock的公平锁

ReentranLock类实现了Lock接口,重写了里面的方法,对于ReentranLock类中的公平锁,我们可以看到如下源码:

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

在上述构造方法中,新建锁对象是否为公平锁,在于传入的参数是true还是false,在三目运算符中,如果传入参数为true,则会创建一个FairSync()对象赋值给sync,线程获取的锁是公平锁;如果为false则创建一个NonfairSync()对象,线程获取的锁是非公平锁。点入FairSync()方法,得到如下源码:

    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);
        }
        /*这里将acquire方法放于此
            public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
        */
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            //拿到当前锁对象的状态
            int c = getState();
            //如果没有线程获取到锁
            if (c == 0) {
                //首先会判断是否有前驱节点,如果没有就会调用CAS机制更新锁对象状态
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    //设置当前线程拥有锁资源
                    setExclusiveOwnerThread(current);
                    //如果获取锁资源成功则返回true
                    return true;
                }
            }
            //或者如果拿到锁的就是当前线程
            else if (current == getExclusiveOwnerThread()) {
                //将锁的状态+1,考虑到锁重入
                int nextc = c + acquires;
                if (nextc < 0)
                    //超过了最大锁的数量
                    throw new Error("Maximum lock count exceeded");
                //如果锁数量没有溢出,设置当前锁状态
                setState(nextc);
                //如果成功获取锁资源则返回true
                return true;
            }
            //如果前两条都不符合,则返回false
            return false;
        }
    }

上述代码涉及到了hasQueuedPredecessors()方法,返回true则代表有有前驱节点,点入查看得到以下源码:

    public final boolean hasQueuedPredecessors() {
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        //h!=t,首节点不等于尾节点表示队列中有节点
        return h != t &&
            (
            //s为头结点的下一个节点,返回false表示队列中还有第二个节点,或运算符后面表示,第二个线程不是当前线程
            (s = h.next) == null || s.thread != Thread.currentThread()
            );
    }

因此做出总结,使用ReentranLock的公平锁,当线程调用ReentranLock类中的lock()方法时,会首先调用FairSync类中的lock()方法;然后FairSync类中的lock()方法会调用AQS类(AbstractQueuedSynchronizer)中的acquire(1)方法获取资源,前面的文章里也提到过,acquire()方法会调用tryAcquire()方法尝试获取锁,tryAcquire()方法里面没有具体的实现,tryAcquire()方法具体逻辑是由其子类实现的,因此调用的还是FairSync类中的方法。在AQS中的acquire()方法中如果尝试获取资源失败,会调用addWaiter()方法将当前线程封装为Node节点放到等待队列的尾部,并且调用AQS中的acquireQueued方法使线程在等待队列中排队。

ReentranLock的非公平锁

非公平锁就是所有抢占锁的线程都是不公平的,在我们日常生活中就相当于是插队现象,不过也与插队稍微不同。在多线程并发时,每个线程在抢占锁的过程中,都会先尝试获取锁,如果获取成功,则直接指向具体的业务逻辑;如果获取锁失败,则会像公平锁一样在等待队列队尾等待。

在非公平锁模式下,由于刚来的线程可以在队首位置进行一次插队,所以当插队成功时,后面的线程可能会出现长时间等待,无法获取锁资源产生饥饿现象。但是非公平锁性能比公平锁性能更好。

对于ReentranLock类中的非公平锁,实现方式有两种,一种是默认的构造方式,另一种是和上面的一样,源码如下:

    //第一种方式,默认实现
    public ReentrantLock() {
        sync = new NonfairSync();
    }
    //第二种方式,传入false来创建非公平锁对象
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

对于创建的NonfairSync()类对象,其源码如下:

    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

在这里插入图片描述

在上述代码中,当线程获取锁时,并没有直接将当前线程放入等待队列中,而是先尝试获取锁资源,如果获取锁成功,设置state标志位1成功,则直接将当前线程拿到锁,执行线程业务;如果获取锁资源失败,则调用AQS中的acquire方法获取资源,而acquire()方法会回调上面NonfairSyn类中的tryAcquire()方法,然后又会回调Sync类中的nonfairTryAcquire()方法(NonfairSync类继承了Sync类) ,点击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;
        }

由上诉代码,没有将线程放入到等待队列中,只是对锁的状态进行了判断,若标识为0,代表没有线程拿到锁,当前线程会使用CAS机制改变锁状态,并调用setExclusiveOwnerThread(current)方法让当前线程拿到锁。

因此综上所述,在使用ReentranLock中的非公平锁时,首先会调用lock()方法,,而ReentranLock类中lock()方法会调用NonfairSync类中的lock()方法,接着NonfairSync类中的lock()方法会调用AQS中的acquire()方法来获取锁资源,AQS中的acquire()方法又会回调NonfairSync类中tryAcquire()方法尝试获取资源,NonfairSync类中tryAcquire()方法会调用Sync类中的nonfairTryAcquire方法尝试非公平锁获取资源;获取失败的话,AQS中的acquire()方法会调用addWaiter()方法将当前线程封装成Node节点放入到等待队列的队尾,而后AQS中的acquire()方法会调用AQS中的acquireQueued()方法让线程在等待队列中排队。这就是非公平锁的整个流程。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。