• 深入了解ReentrantLock中的公平锁和非公平锁的加锁机制


    ReentrantLock和synchronized一样都是实现线程同步,但是像比synchronized它更加灵活、强大、增加了轮询、超时、中断等高级功能,可以更加精细化的控制线程同步,它是基于AQS实现的锁,他支持公平锁和非公平锁,同时他也是可重入锁和自旋锁。

    本章将基于源码来探索一下ReentrantLock的加锁机制,文中如果存在理解不到位的地方,还请提出宝贵意见共同探讨,不吝赐教。

    公平锁和非公平锁的加锁机制流程图:

    一、ReentrantLock的公平锁

    使用ReentrantLock的公平锁,调用lock进行加锁,lock方法的源码如下:

    final void lock() {
        acquire(1);
    }

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

    可以看到,FairLock首先调用了tryAcquire,tryAcquire源码如下:

    /**
     * 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) {
            //如果队列中不存在等待的线程或者当前线程在队列头部,则基于CAS进行加锁
            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;
    }

    从源码中可以看到,当state为0,即没有线程获取到锁时,FairLock首先会调用hasQueuedPredecessors()方法检查队列中是否有等待的线程或者自己是否在队列头部,如果队列中不存在等待的线程或者自己在队列头部则调用compareAndSetState()方法基于CAS操作进行加锁,如果CAS操作成功,则调用setExclusiveOwnerThread设置加锁线程为当前线程。

    当state不为0,即有线程占用锁的时候会判断占有锁的线程是否是当前线程,如果是的话则可以直接获取到锁,这就是ReentrantLock是可重入锁的体现。

    如果通过调用tryAcquire没有获取到锁,从源码中我们可以看到,FairLock会调用addWaiter()方法将当前线程加入CLH队列中,addWaiter方法源码如下:

    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;
            //基于CAS将当前线程节点加入队列尾部
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //如果CAS操作失败,则调用enq自旋加入队列
        enq(node);
        return node;
    }

    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;
                    }
                }
            }
      }

    在addWaiter方法中,会CAS操作将当前线程节点加入队列尾部,如果第一次CAS失败,则会调用enq方法通过自旋的方式,多次尝试进行CAS操作将当前线程加入队列。

    将当前线程加入队列之后,会调用acquireQueued方法实现当前线程的自旋加锁,acquireQueued源码如下:

    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);
            }
        }

    在acquireQueued方法中每次自旋首先会调用predecessor()方法获取,当前线程节点的前节点,如果发现前节点是head节点,则说明当前线程节点处于对头(head是傀儡节点),那么则调用tryAcquire尽心加锁。

    如果当前线程节点不在队列头部,那么则会调用shouldParkAfterFailedAcquire方法判断当前线程节点是否可以挂起知道前节点释放锁时唤醒自己,如果可以挂起,则调用parkAndCheckInterrupt实现挂起操作。

    shouldParkAfterFailedAcquire源码如下:

    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;
        }

    shouldParkAfterFailedAcquire源码中,如果当前线程节点的前节点的waitStatus状态为SIGNAL(-1)时,表明前节点已经设置了释放锁时唤醒(unpark)它的后节点,那么当前线程节点可以安心阻塞(park),等待它的前节点在unlock时唤醒自己继续尝试加锁。

    如果前节点的waitStatus状态>0,即为CANCELLED (1),表明前节点已经放弃了获取锁,那么则会继续往前找,找到一个能够在unlock时唤醒自己的线程节点为止。如果前节点waitStatus状态是CONDITION (-2),即处于等待条件的状态,则会基于CAS尝试设置前节点状态为SIGNAL(主动干预前节点达到唤醒自己的目的)。

    parkAndCheckInterrupt源码:

    private final boolean parkAndCheckInterrupt() {
            LockSupport.park(this);
            return Thread.interrupted();
        }

    二、ReentrantLock的非公平锁

    和公平锁加锁机制不同的是,非公平锁一上来不管队列中是否还存在线程,就直接使用CAS操作进行尝试加锁(这就是它的非公平的体现),源码如下:

     final void lock() {
         if (compareAndSetState(0, 1))
             setExclusiveOwnerThread(Thread.currentThread());
         else
             acquire(1);
    }

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

    如果CAS操作失败(一上来就吃了个闭门羹),则调用acquire方法进行后续的尝试和等待。从源码中可以看到,首先回调用tryAcquire方法进行再次尝试加锁或者锁重入,NoFairLockd的tryAcquire方法源码如下:

    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;
            }

    可以看到NoFairLock的tryAcquire方法和FairLock的tryAcquire方法唯一不同之处是NoFairLock中尝试加锁前不需要调用hasQueuedPredecessors方法判断队列中是否存在其他线程,而是直接进行CAS操作加锁。

    那么如果再次尝试加锁或者锁重入失败,则会进行后续的和公平锁完全一样的操作流程(不再赘述),即:加入队列(addWaiter)–>自旋加锁(acquireQueued)。另外,关注Java知音公众号,回复“后端面试”,送你一份面试题宝典!

    三、unlock解锁

    说完了公平锁和非公平锁的加锁机制,我们再顺带简单的看看解锁源码。unlock源码如下:

    public void unlock() {
            sync.release(1);
    }

    public final boolean release(int arg) {
            //尝试释放锁
            if (tryRelease(arg)) {
                Node h = head;
                //锁释放成后唤醒后边阻塞的线程节点
                if (h != null && h.waitStatus != 0)
                    unparkSuccessor(h);
                return true;
            }
            return false;
    }

    总结 本文主要探索了公平锁和非公平锁的加锁流程,他们获取锁的不同点和相同点。整篇文章涉及到了以下几点:

    1. 公平锁、非公平锁加锁过程
    2. 自旋锁的实现以及自旋过程中的阻塞唤醒
    3. 可重入锁的实现
    4. CLH队列

    转载:blog.csdn.net/qq_40400960/article/details/114242448

  • 相关阅读:
    MySQL(错误1064)
    如何判断是手机还是电脑访问网站
    Oracle表分区
    分离Date数据
    多对多
    一对多
    SQLalchemy基础
    paramiko上传下载
    paramiko
    automap
  • 原文地址:https://www.cnblogs.com/fengyun2050/p/14955915.html
Copyright © 2020-2023  润新知