ReentrantLock可重入锁

Lock

java.util.concurrent.lock 中的 Lock 框架是锁定的一个抽象,它允许把锁定的实现作为 Java 类,而不是作为语言的特性来实现。这就为 Lock 的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义。

ReentrantLock是什么?

ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等的一些特性。当许多线程都想访问共享资源时,JVM可以花更少的时候来调度线程,把更多时间用在执行线程上,因此可以获得更好的性能。

AbstractQueuedSynchronizer

ReentrantLock实现的前提就是AbstractQueuedSynchronizer,简称AQS,是java.util.concurrent的核心,CountDownLatchFutureTaskSemaphoreReentrantLock等都有一个内部类是这个抽象类的子类。由于AQS是基于FIFO队列的实现,因此必然存在一个个节点,Node就是一个节点。

ReentrantLock使用场景

  • 如果发现该操作已经在执行中则不再执行。
  • 如果发现该操作已经在执行,等待一个一个执行。
  • 如果发现该操作已经在执行,则尝试等待一段时间,等待超时则不执行。
  • 如果发现该操作已经在执行,等待执行。这时可中断正在进行的操作立刻释放锁继续下一操作。

ReentrantLock实现原理

ReentrantLock中有一个抽象类Sync:

private final Sync sync;

    /**
     * Base of synchronization control for this lock. Subclassed
     * into fair and nonfair versions below. Uses AQS state to
     * represent the number of holds on the lock.
     */
    abstract static class Sync extends AbstractQueuedSynchronizer {
    ...
    }

ReentrantLock根据传入构造方法的布尔型参数实例化出Sync的实现类FairSync和NonfairSync,分别表示公平的Sync和非公平的Sync。

这里顺带提一下公平锁,就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己。而非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。

ReentrantLock非公平锁
lock()

由于ReentrantLock我们用的比较多的是非公平锁,所以看下非公平锁是如何实现的。

假设线程A调用了ReentrantLock的lock()方法,此时没有任何线程持有锁,那么线程A就持有了这个锁。调用链如下:

image

第一个获取锁的线程就做了两件事情:

  • 设置AbstractQueuedSynchronizer的state为1
  • 设置AbstractOwnableSynchronizer的thread为当前线程

这两步做完之后就表示线程1独占了锁。然后线程2也要尝试获取同一个锁,在线程1没有释放锁的情况下必然是行不通的,所以线程2就要阻塞。那么,线程2如何被阻塞?看下线程2的方法调用链,这就比较复杂了:
image

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

线程2首先还是使用CAS尝试获取锁,如果state为0就将AbstractQueuedSynchronizer的state设置为1。因为线程1在持有锁的时候已经将state设置为1了,所以状态设置会失败。线程2执行acquire()

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

这个方法做了两件事情:1.再次尝试获取一次锁,如果获取的结果为false即为失败,做第2件事情;2.添加FIFO等待队列。tryAcquire()方法最后调用的是Sync的nonfairTryAcquire(int 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;
}

由于state是volatile的,所以state对于线程2是可见的。线程2再次尝试获取锁,有可能在线程2第一次获取锁失败到再次获取锁的过程中,线程1已经执行完代码将锁释放了。如果锁的状态不0,则判断线程2是否与正持有锁的进程是同一个。如果是,则将state+1,由于是重入,所以这里不会产生竞争,因此也不需要利用CAS设置state。从这个方法中我们可以了解到每次调用state都会加1,也就是nextc加1,当nextc<0的时候会抛出Error。也就是说同一个锁最多只能重入Integer.MAX_VALUE次(2147483647次)。

最后代码执行到AQS的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;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

为当前的线程创建一个Node,模式为独占模式(Node.Exclusive == null)。
再判断一下此时队列上是否有节点,没有的话就创建一个等待队列,执行enq():

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

取队列尾节点tail,如果此时队列上没有任何节点,创建头结点head并将tail指向head。如果队列上存在节点,则将传进来的node.prev指向tail,并将tail.next指向node。


note:

enq方法采用的是变种CLH算法,先看头结点是否为空,如果为空就创建一个傀儡结点,头尾指针都指向这个傀儡结点,这一步只会在队列初始化时会执行;如果头结点非空,就采用CAS操作将当前结点插入到头结点后面,如果在插入的时候尾结点有变化,就将尾结点向后移动直到移动到最后一个结点为止,然后再把当前结点插入到尾结点后面,尾指针指向当前结点,入队成功。


由于线程2所在的Node是第一个要等待的Node,因此FIFO队列上肯定没有内容,tail为null,走的就是if条件通过的代码逻辑。这里用了CAS设置头Node,当然有可能线程2设置头Node的时候CPU切换了,线程3已经把头Node设置好了形成了上图所示的一个队列,这时线程2再循环一次获取tail,由于tail是volatile的,所以对线程2可见,线程2看见tail不为null,就走到了else里面去往尾Node后面添加自身。整个过程下来,形成了一个双向队列。最后走AQS的acquireQueued(node, 1):

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

因为线程2是双向队列的真正的第一个Node(前面还有一个h),所以再次尝试一下线程2能不能获取锁(可能这段时间内线程1已经执行完了把锁释放了,state从1变为了0),如果还是获取失败,先调用AQS的shouldParkAfterFailedAcquire(p, 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;
}

该方法首先检查前趋结点的waitStatus位,如果为SIGNAL,表示前趋结点会通知它,那么它可以放心大胆地挂起了;
如果前趋结点是一个被取消的结点怎么办呢?那么就向前遍历跳过被取消的结点,直到找到一个没有被取消的结点为止,将找到的这个结点作为它的前趋结点,将找到的这个结点的waitStatus位设置为SIGNAL,返回false表示线程不应该被挂起。

这个waitStatus是h的waitStatus,很明显是0,所以此时把h的waitStatus设置为Noed.SIGNAL即-1并返回false。既然返回了false,上面的acquireQueued的11行if自然不成立,再走一次for循环,还是先尝试获取锁,不成功,继续走shouldParkAfterFailedAcquire,此时waitStatus为-1,小于0,走第三行的判断,返回true。然后走acquireQueued的11行if的第二个判断条件parkAndCheckInterrupt:

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}
public static void park(Object blocker) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);
    unsafe.park(false, 0L);
    setBlocker(t, null);
}

最后一步,调用LockSupport的park方法阻塞住了当前的线程。整个线程1独占锁,将线程2加入到FIFO队列并将其阻塞如上所述。

unlock()

调用ReentrantLock 的unLock()方法:

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

调用AQS的release()

public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
}

执行Sync的tryRelease()

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

首先,只有当c==0的时候才会让free=true,这和上面一个线程多次调用lock方法累加state是对应的,调用了多少次的lock()方法自然必须调用同样次数的unlock()方法才行,这样才把一个锁给全部解开。
当一条线程对同一个ReentrantLock全部解锁之后,AQS的state自然就是0了,AbstractOwnableSynchronizer的exclusiveOwnerThread将被设置为null,这样就表示没有线程占有锁,方法返回true。

代码继续往下走,上面的release方法的第四行,h不为null成立(有等待队列),h的waitStatus不为0(h不为新节点)也成立,所以走第5行的unparkSuccessor方法:

private void unparkSuccessor(Node node) {
    /*
     * If status is negative (i.e., possibly needing signal) try
     * to clear in anticipation of signalling.  It is OK if this
     * fails or if status is changed by waiting thread.
     */
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    /*
     * Thread to unpark is held in successor, which is normally
     * just the next node.  But if cancelled or apparently null,
     * traverse backwards from tail to find the actual
     * non-cancelled successor.
     */
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);
}

node参数传进来的是头结点,首先检查头结点的waitStatus位,如果为负,表示头结点还需要通知后继结点,这里不需要头结点去通知后继,因此将该标志位清0.

然后查看头结点的下一个结点,如果下一个结点不为空且它的waitStatus<=0,表示后继结点没有被取消,是一个可以唤醒的结点,于是唤醒后继结点返回;如果后继结点为空或者被取消了怎么办?寻找下一个可唤醒的结点,然后唤醒它返回。
这里并没有从头向尾寻找,而是相反的方向寻找,为什么呢?
因为在CLH队列中的结点随时有可能被中断,被中断的结点的waitStatus设置为CANCEL,而且它会被踢出CLH队列,如何个踢出法,就是它的前趋结点的next并不会指向它,而是指向下一个非CANCEL的结点,而它自己的next指针指向它自己。一旦这种情况发生,如何从头向尾方向寻找继任结点会出现问题,因为一个CANCEL结点的next为自己,那么就找不到正确的继任接点。

tryRelease方法主要是做了一个释放锁的过程,将同步状态state -1,直到减到0为止,这主要是兼容重入锁设计的,同时setExclusiveOwnerThread(null)清除当前占用的线程。这些head节点后的线程和新进的线程就可以开始争抢。这里需要注意的是对于同步队列中的线程来说在setState(c),且c为0的时候,同步队列中的线程是没有竞争锁的,因为线程被park了还没有唤醒。但是此时对于新进入的线程是有机会获取到锁的。

因为在setState(c)释放了锁之后,是没有线程竞争的,所以head是当前的head节点,先检查当前的Node是否合法,如果合法则unpark it。(阻塞的线程只有被唤醒之后才会继续执行相关代码)开始锁的获取。就回到了上面的for循环执行获取锁逻辑:

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);
    }
}
private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}

setHead方法里面的前驱Node是Null,也没有线程,那么为什么不用一个在等待的线程作为Head Node呢?

因为一个线程随时有可能因为中断而取消,而取消的话,Node自然就要被GC了,那GC前必然要把头Node的后继Node变为一个新的头而且要应对多种情况,这样就很麻烦。用一个没有thread的Node作为头,相当于起了一个引导作用,因为head没有线程,自然也不会被取消。

再看一下上面unparkSuccessor的14行~20行,就是为了防止head的下一个node被取消的情况,这样,就从尾到头遍历,找出离head最近的一个node,对这个node进行unPark操作。

总结下来,如果线程1要释放锁,会先将锁的state减到0为止,并在等待队列里面找到一个最前面的未被取消的已被阻塞的节点,将其唤醒并获取锁。这个线程就是线程2.

ReentrantLock的公平锁实现与非公平锁实现比较相似,最大的差别就是新的线程获取锁时不能插队,必须老老实实加入到等待队列中。不再重复叙述。

这篇博文综合了几篇网上的博文,再加上一定自己的理解写的。有重复的部分侵删。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值