Java公平锁与非公平锁的核心原理讲解

从公平的角度来说,Java中的锁总共可分为两类:公平锁和非公平锁。但公平锁和非公平锁有哪些区别?核心原理是什么?本文就来和大家详细聊聊

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()方法让线程在等待队列中排队。这就是非公平锁的整个流程。

到此这篇关于Java公平锁与非公平锁的核心原理讲解的文章就介绍到这了,更多相关Java公平锁 非公平锁内容请搜索编程学习网以前的文章希望大家以后多多支持编程学习网!

本文标题为:Java公平锁与非公平锁的核心原理讲解

基础教程推荐