Java多线程与高并发六(Lock上锁底层实现原理)

     我们前面介绍了Lock的基本用法,知道Lock有公平锁、非公平锁两种实现,也知道Lock底层是用CAS实现的,但我们前面并没有详细介绍底层实现,本文就围绕Lock的加锁操作介绍Lock的底层实现原理。

     我们的切入点是可重入锁ReentrantLock的lock()方法:

Lock lock = new ReentrantLock();
lock.lock();

跟进代码可以看到如下源码:

public void lock() {
    sync.lock();
}

sync是ReentrantLock的一个抽象静态内部类:

abstract static class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = -5179523762034025860L;

    /**
     * Performs {@link Lock#lock}. The main reason for subclassing
     * is to allow fast path for nonfair version.
     */
    abstract void lock();
//此处省略源码无数
}

Sync继承的类AQS是今天本文的主角!大家掌声有请!

而代码看到这里,我们就要开始区分sync到底是公平的还是非公平的,看看ReentrantLock的构造方法:

    /**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

哦,原来直接new ReentrantLock是非公平的实现,而传入参数true、false的构造方法里面则是一个三元运算,true就new一个公平锁实现,由于公平锁上锁实现更为复杂,我们看看公平锁的lock方法干了些什么事:

    /**
     * Sync object for fair locks
     */
    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);
        }
    //此处省略代码无数
    }

目前感觉还很幸福,原来公平锁的lock方法这么简单,就是要获得1而已。哈哈,源码真简单!

Java多线程与高并发六(Lock上锁底层实现原理)_第1张图片

FairSync继承Sync,Sync又继承AQS,所以公平锁调用的acquire(1)方法是爸爸的呢,还是爷爷的呢?继续跟进:

    /**
     * Acquires in exclusive mode, ignoring interrupts.  Implemented
     * by invoking at least once {@link #tryAcquire},
     * returning on success.  Otherwise the thread is queued, possibly
     * repeatedly blocking and unblocking, invoking {@link
     * #tryAcquire} until success.  This method can be used
     * to implement method {@link Lock#lock}.
     *
     * @param arg the acquire argument.  This value is conveyed to
     *        {@link #tryAcquire} but is otherwise uninterpreted and
     *        can represent anything you like.
     */
    public final void acquire(int arg) {
        if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)){
             selfInterrupt();
        }    
    }

可以看到acquire其实是公平锁的爷爷辈AQS提供的,看看注释所说:(以下内容为作者不标准翻译,务必只是参考)

说这个acquire方法啊,忽略线程打断的情况,以独占模式获得。通过调用另外一个方法tryAcquire至少一次来实现的,成功的时候就返回(也没有返回值啊),否则这个线程就queued排队呗,可能在阻塞、非阻塞两种状态来回切换,直到调用tryAcquire获得成功才停止切换状态,这个方法能够被用来实现lock方法!参数arg用来传递给另外一个tryAcquire方法,可以代表你喜欢的anything。。。

读到这里,有没有觉得虽然代码很少,但是却不易懂了。。。

Java多线程与高并发六(Lock上锁底层实现原理)_第2张图片

没关系,我们先看看if这个条件如果成立,会发生什么事?

    /**
     * Convenience method to interrupt current thread.
     */
    static void selfInterrupt() {
        Thread.currentThread().interrupt();
    }

正如注释所说:selfInterrupt方法是一个方便方法,用来打断当前线程。(作者说:老外真是的会省代码,一句代码非得抽成一个方法名).

Thread的interrupt()方法,只是用来设置一个打断的状态,以下是原文:

If none of the previous conditions hold then this thread's interrupt status will be set.

 也就是说,acquire方法是用来实现lock方法的,公平锁的实现呢,acquire参数传的1,1又是传给tryAcquire方法的,当尝试获得锁成功(!tryAcquire(1)就是false),if条件也就不成立了,打断当前线程也不必了,锁就获得了,方法也结束了,对!就这么结束了。

前面acquire的注释说到,acquire方法会调用不止一次的tryAcquire方法来获得锁,那么事情肯定没这么简单,如果if条件中,tryAcquire方法返回的是false呢??(!tryAcquire(1)就是true),根据逻辑与(&&)的判断规则,acquireQueued方法返回true,就意味着要给当前线程设置一个打断的标志位,标志当前线程被打断过,至于作用我们这里按下不表。

我们先看看尝试获得锁的逻辑,这段代码在公平锁FairSync里,实现的爷爷AQS的protected方法:

        /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                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;
        }
    }

注释说到,这是公平锁的的tryAcquire版本(因为有多个子类继承AQS的tryAcquire方法),不保证方法调用通过,除非循环调用成功或者队列没有等待的线程或者是第一个线程。

来不及解释了,先记住AQS的结构:

Java多线程与高并发六(Lock上锁底层实现原理)_第3张图片

AQS由一个volatile的int类型变量state和双向链表组成。链表中装的线程。

tryAcquire方法的逻辑很简单:

1、拿到当前线程

2、拿到AQS中表示锁状态的int类型字段state

3、判断state是否等于0,0表示没加锁。

如果等于0

3.1如果链表中没有排队的前驱结点并且通过CAS操作能够把0变为1,恭喜,获得锁成功,并且把当前线程设置为独占线程。这段逻辑是AQS的爸爸AbstractOwnableSynchronizer的,要说这个setExclusiveOwnerThread()方法的作用就是往AOS里面的Thread成员变量exclusiveOwnerThread设值,解锁的时候就把这个值置为null。

3.2如果链表中有排队的前驱结点,或者通过CAS没有把0变为1(有可能别的线程抢先获得锁成功),返回fasle,tryAcquire方法尝试获得锁失败。

4、如果第1步中的线程与AOS中的exclusiveOwnerThread线程相等,就把第2步中拿到的state加1,再设置state为相加过后的数值。

不知大家有没有看出来,第3步就是获得锁,第4步表示锁的重入。

说的大白话一点,获得锁就是把AQS的state改为1,锁的重入就是在1的基础上继续加。

tryAcquire方法整明白了,我们再回顾看一下acquire方法:

    public final void acquire(int arg) {
        if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)){
             selfInterrupt();
        }    
    }

如果tryAcquire获得锁失败了,还得执行acquireQueued方法,那么这个方法又在干嘛?为嘛参数还是一个方法。

Java多线程与高并发六(Lock上锁底层实现原理)_第4张图片

源码还在继续。。。

假设我们tryAcquire失败了, 也就是if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))该执行后面的acquireQueued方法了。

现在看看这个acquireQueued方法到底在干嘛?

    /**
     * Acquires in exclusive uninterruptible mode for thread already in
     * queue. Used by condition wait methods as well as acquire.
     *
     * @param node the node
     * @param arg the acquire argument
     * @return {@code true} if interrupted while waiting
     */
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()){
                    interrupted = true;
                }
                   
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

方法注释说到,该方法是以独占非打断模式获得队列中的线程。

我们先忽略failed和interrupted标志位,先看看死循环里干了啥:

1、拿到方法传入的node的前驱结点p

2、判断p是否是头结点,判断是否tryAcquire是否成功

2.1如果传入结点的前驱结点p是头结点,也就是说方法传入的结点node是链表中第二个结点,这时并且tryAcquire也成功了(表示头结点此时已经没有持有锁了),就把当前结点设置为头结点,把头结点的后继结点设置为null(帮助垃圾回收)。注意了,这里返回的interrupted并没有被改动,还是false,当前线程标志位不会被设置为打断状态

2.2假如传入结点node的前驱结点不是头结点或者尝试获得锁失败,通过一个if条件,有可能改变interrupted的值为true,如果这个值被返回,就表示当前线程设置了打断的标志位

这也就意味着,这个if条件是一种更改当前线程标志位的原因

啊哈,两个方法同时成立才能够改变当前线程的标志位。好麻烦啊,有木有?

Java多线程与高并发六(Lock上锁底层实现原理)_第5张图片

好,我们继续,我倒要看看这个导致当前线程打断标志位变更的原因是什么?

说这事之前,我们先看看AQS的Node长啥样?

    static final class Node {
        /** Marker to indicate a node is waiting in shared mode */
        static final Node SHARED = new Node();
        /** Marker to indicate a node is waiting in exclusive mode */
        static final Node EXCLUSIVE = null;

        /** waitStatus value to indicate thread has cancelled */
        static final int CANCELLED =  1;
        /** waitStatus value to indicate successor's thread needs unparking */
        static final int SIGNAL    = -1;
        /** waitStatus value to indicate thread is waiting on condition */
        static final int CONDITION = -2;
        /**
         * waitStatus value to indicate the next acquireShared should
         * unconditionally propagate
         */
        static final int PROPAGATE = -3;

        volatile int waitStatus;

        volatile Node prev;

        volatile Node next;

        /**
         * The thread that enqueued this node.  Initialized on
         * construction and nulled out after use.
         */
        volatile Thread thread;

        Node nextWaiter;

        /**
         * Returns true if node is waiting in shared mode.
         */
        final boolean isShared() {
            return nextWaiter == SHARED;
        }

        /**
         * Returns previous node, or throws NullPointerException if null.
         * Use when predecessor cannot be null.  The null check could
         * be elided, but is present to help the VM.
         *
         * @return the predecessor of this node
         */
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }

        Node() {    // Used to establish initial head or SHARED marker
        }

        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }

        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

 刨去了大段的注释,我们可以看到:

1、node有两个用自身做的标记,一个SHARED,一个EXCLUSIVE,说白了就是共享锁和排它锁呗。

2、node有多种状态,1表示取消获得锁,-1表示后继结点在等待当前结点通知可以获得锁了,-2表示线程调用了Condition的await()方法等待中,-3表示共享模式下node应无条件传播状态值,让后面的线程都能争抢锁

回到刚刚我们说的,这个if条件是一种更改当前线程标志位的原因。

acquireQueued方法更改当前线程标志位两个条件之一:shouldParkAfterFailedAcquire(node的前驱结点,node),注意方法的入参  

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

拿到node的前驱结点的等待状态

1、判断状态是否是SINGAL,如果是,表示后继节点在等待当前结点的唤醒,但是后继结点node还是阻塞状态,这里返回true,表示node尝试获得锁失败后,应该park的意思

2、不是SINGAL,那线程是不是取消锁的争用了呢?如果取消了,就把node的前驱结点的前驱结点置为node的前驱结点,循环判断,直到找到一个前驱节点不是取消状态的为止。说白了就是从链表中删除取消的node

3、其余状态就把node的前驱结点状态设置为SINGAL

4、只有node前驱节点是SINGAL状态返回true,其余都是返回false,返回fales表示accquireQueued方法中死循环部分的第二个if条件不通过,也就是interrupted不会改变状态,还是false,这时获得锁,当前线程标志位就不会改变。

acquireQueued方法更改当前线程标志位两个条件之二:parkAndCheckInterrupt()

    /**
     * Convenience method to park and then check if interrupted
     *
     * @return {@code true} if interrupted
     */
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

还好这个方法很简单,就是阻塞当前线程,拿到当前是否有打断状态并清除当前线程的打断标志位。这里必须说明下,Thread有三个关于interrupt的方法:

1、interrupt()  给当前线程设置一个打断的标志位

2、interrupted()  返回当前线程的打断标志位,被打断过就返回true,否则返回false,但是会清除打断状态,比如:之前A线程被B线程打断过,第一次调用A线程的interrupted方法,返回true,第二次调用就返回false了

In other words, if this method were to be called twice in succession, the second call would return false (unless the current thread were interrupted again, after the first call had cleared its interrupted status and before the second call had examined it.)

3、isInterrupted()  只返回当前线程是否被打断过,不清除打断状态

Tests whether this thread has been interrupted.  The interrupted status of the thread is unaffected by this method.

那我们现在明白了,这两个条件其实就是想表达,当前线程在AQS的链表中排队,如果当前node不是链表中第二个node,就检查前驱节点的状态,把前驱结点状态更新为SINGAL,并且park自身,清除当前线程的打断状态,再依据当前线程是否被打断过来更改当前线程的状态,假如当前线程被打断过,parkAndCheckInterrupt方法返回true,if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())字句通过,interrupted标志位被修改为true,如果这时获得锁,就会把interrupted返回到acquireQueued方法,也就会执行打断当前线程的selfInterrupt方法,相当于当前线程如果被打断过,就刷新一下打断的状态

检查当前结点的前驱结点是否是头结点检查当前结点的前驱结点状态是否是SINGAL并且更新其前驱结点状态这两件事是在循环做,直到返回一个boolean类型值为止。acquireQueued方法返回false,表示获得锁,并且不刷新当前线程的打断状态;返回true,也表示获得锁,但是刷新当前线程的被打断状态。

我们一直没有提addWaiter这个方法,现在回过头看看添加线程进入等待队列里是怎么实现的?

 /**
     * Inserts node into queue, initializing if necessary. See picture above.
     * @param node the node to insert
     * @return node's predecessor
     */
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

    /**
     * Creates and enqueues node for current thread and given mode.
     *
     * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
     * @return the new node
     */
    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

方法传入Node的标记,排它锁标记,并且以排他锁标记new了一个node

1、拿到队列尾巴结点

2、如果尾巴结点不是空,意味着有线程排队呢

挂载node到尾巴结点(双向链表操作,我的后继结点是你,你的前驱结点是我),并且通过CAS的方式,把tail指向node

3、如果尾巴结点是空的情况,调用enq(node)入队方法

4、返回node,注意这个node就是传入acquireQueued方法的node

enq方法就是把node添加进链表的方法,会判断是否链表为空,为空则头结点和尾结点指针指向一起。注意这个方法是个死循环,不达目的死不罢休!

至此,lock的lock()方法算是完毕了,不知道你有没有被绕晕呢?最后总结下吧:

Java多线程与高并发六(Lock上锁底层实现原理)_第6张图片

你可能感兴趣的:(Java并发编程)