从源码角度完全理解ReentrantLock

前言

ReentrantLock能够有公平锁和非公平锁的不一样实现,只要在构造它的时候传入不一样的布尔值,继续跟进下源码咱们就能发现,关键在于实例化内部变量sync的方式不一样,以下所示java

/**
 * 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();
}

公平锁内部是FairSync,非公平锁内部是NonfairSync。而无论是FairSync仍是NonfariSync,都间接继承自AbstractQueuedSynchronizer这个抽象类,以下图所示node

  • NonfairSync的类继承关系

  • FairSync的类继承关系

该抽象类为咱们的加锁和解锁过程提供了统一的模板方法,只是一些细节的处理由该抽象类的实现类本身决定。因此在解读ReentrantLock(重入锁)的源码以前,有必要了解下AbstractQueuedSynchronizer。算法

AbstractQueuedSynchronizer介绍

AQS是构建同步组件的基础

AbstractQueuedSynchronizer,简称AQS,为构建不一样的同步组件(重入锁、读写锁、CountDownLatch、Semphore等)提供了可扩展的基础框架,以下图所示。编程

AQS以模板方法模式在内部定义了获取和释放同步状态的模板方法,并留下钩子函数供子类继承时进行扩展,由子类决定在获取和释放同步状态时的细节,从而实现知足自身功能特性的需求。除此以外,AQS经过内部的同步队列管理获取同步状态失败的线程,向实现者屏蔽了线程阻塞和唤醒的细节。安全

CAS算法是AbstractQueuedSynchronizer的核心。数据结构

AQS的内部结构(ReentrantLock的语境下)

AbstractQueuedSynchronizer类底层的数据结构是使用双向链表,是队列的一种实现,故也可当作是队列,其中Sync queue,即同步队列,是双向链表,包括head结点和tail结点,head结点主要用做后续的调度。而Condition queue不是必须的,其是一个单向链表,只有当使用Condition时,才会存在此单向链表。而且可能会有多个Condition queue。并发

同步等待队列

AQS中同步等待队列的实现是一个带头尾指针(这里用指针表示引用是为了后面讲解源码时能够更直观形象,何况引用自己是一种受限的指针)且不带哨兵结点(后文中的头结点表示队列首元素结点,不是指哨兵结点)的双向链表。app

/**
 * Head of the wait queue, lazily initialized.  Except for
 * initialization, it is modified only via method setHead.  Note:
 * If head exists, its waitStatus is guaranteed not to be
 * CANCELLED.
 */
private transient volatile Node head;//指向队列首元素的头指针

/**
 * Tail of the wait queue, lazily initialized.  Modified only via
 * method enq to add new wait node.
 */
private transient volatile Node tail;//指向队列尾元素的尾指针

head是头指针,指向队列的首元素;tail是尾指针,指向队列的尾元素。而队列的元素结点Node定义在AQS内部,主要有以下几个成员变量框架

volatile Node prev;      //指向前一个结点的指针
volatile Node next;      //指向后一个结点的指针

volatile Thread thread;  //当前结点表明的线程
volatile int waitStatus; //等待状态
  • prev:指向前一个结点的指针
  • next:指向后一个结点的指针
  • thread:当前结点表示的线程,由于同步队列中的结点内部封装了以前竞争锁失败的线程,故而结点内部必然有一个对应线程实例的引用
  • waitStatus:对于重入锁而言,主要有3个值。0:初始化状态;-1(SIGNAL):当前结点表示的线程在释放锁后须要唤醒后续节点的线程;1(CANCELLED):在同步队列中等待的线程等待超时或者被中断,取消继续等待。

同步队列的结构以下图所示函数

为了接下来可以更好的理解加锁和解锁过程的源码,对该同步队列的特性进行简单的讲解:

  • 同步队列是个先进先出(FIFO)队列,获取锁失败的线程将构造结点并加入队列的尾部,并阻塞本身。如何才能线程安全的实现入队是后面讲解的重点,毕竟咱们在讲锁的实现,这部分代码确定是不能用锁的。
  • 队列首结点能够用来表示当前正获取锁的线程。
  • 当前线程释放锁后将尝试唤醒后续处结点中处于阻塞状态的线程。

为了加深理解,还能够在阅读源码的过程当中思考下这个问题:

这个同步队列是FIFO队列,也就是说先在队列中等待的线程将比后面的线程更早的获得锁,那ReentrantLock是如何基于这个FIFO队列实现非公平锁的?

AQS中的其余数据结构(ReentrantLock的语境下)

  • 同步状态变量
/**
 * The synchronization state.
 */
private volatile int state;

这是一个带volatile前缀的int值,是一个相似计数器的东西。在不一样的同步组件中有不一样的含义。以ReentrantLock为例,state能够用来表示该锁被线程重入的次数。当state为0表示该锁不被任何线程持有;当state为1表示线程刚好持有该锁1次(未重入);当state大于1则表示锁被线程重入state次。由于这是一个会被并发访问的量,为了防止出现可见性问题要用volatile进行修饰。

  • 持有同步状态的线程标志
/**
 * The current owner of exclusive mode synchronization.
 */
private transient Thread exclusiveOwnerThread;

如注释所言,这是在独占同步模式下标记持有同步状态线程的。ReentrantLock就是典型的独占同步模式,该变量用来标识锁被哪一个线程持有。


了解AQS的主要结构后,就能够开始进行ReentrantLock的源码解读了。因为非公平锁在实际开发中用的比较多,故以讲解非公平锁的源码为主。如下面这段对非公平锁使用的代码为例:

public class NoFairLockTest {
    
    public static void main(String[] args) {
        //建立非公平锁
        ReentrantLock lock = new ReentrantLock(false);
        try {
            //加锁
            lock.lock();
            //模拟业务处理用时
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //释放锁
            lock.unlock();
        }
    }
}

非公平锁加锁流程

加锁流程从lock.lock()开始

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

进入该源码,正确找到sycn的实现类后能够看到真正有内容的入口方法

加锁流程真正意义上的入口

/**
 * Performs lock.  Try immediate barge, backing up to normal
 * acquire on failure.
 */
//加锁流程真正意义上的入口
final void lock() {
    //以cas方式尝试将AQS中的state从0更新为1
    if (compareAndSetState(0, 1))
        //获取锁成功则将当前线程标记为持有锁的线程,而后直接返回
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);//获取锁失败则执行该方法
}

首先尝试快速获取锁,以cas的方式将state的值更新为1,只有当state的原值为0时更新才能成功,由于state在ReentrantLock的语境下等同于锁被线程重入的次数,这意味着只有当前锁未被任何线程持有时该动做才会返回成功。若获取锁成功,则将当前线程标记为持有锁的线程,而后整个加锁流程就结束了。若获取锁失败,则执行acquire方法

/**
 * 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();
}

该方法主要的逻辑都在if判断条件中,这里面有3个重要的方法tryAcquire(),addWaiter()和acquireQueued(),这三个方法中分别封装了加锁流程中的主要处理逻辑,理解了这三个方法到底作了哪些事情,整个加锁流程就清晰了。

尝试获取锁的通用方法 tryAcquire()

tryAcquire是AQS中定义的钩子方法,以下所示

protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

该方法默认会抛出异常,强制同步组件经过扩展AQS来实现同步功能的时候必须重写该方法,ReentrantLock在公平和非公平模式下对此有不一样实现,非公平模式的实现以下:

protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

底层调用了nonfairTryAcquire()
从方法名上咱们就能够知道这是非公平模式下尝试获取锁的方法,具体方法实现以下

/**
 * Performs non-fair tryLock.  tryAcquire is implemented in
 * subclasses, but both need nonfair try for trylock method.
 */
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();//获取当前线程实例
    int c = getState();//获取state变量的值,即当前锁被重入的次数
    if (c == 0) {   //state为0,说明当前锁未被任何线程持有
        if (compareAndSetState(0, acquires)) { //以cas方式获取锁
            setExclusiveOwnerThread(current);  //将当前线程标记为持有锁的线程
            return true;//获取锁成功,非重入
        }
    }
    else if (current == getExclusiveOwnerThread()) { //当前线程就是持有锁的线程,说明该锁被重入了
        int nextc = c + acquires;//计算state变量要更新的值
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);//非同步方式更新state值
        return true;  //获取锁成功,重入
    }
    return false;     //走到这里说明尝试获取锁失败
}

这是非公平模式下获取锁的通用方法。它囊括了当前线程在尝试获取锁时的全部可能状况:

  • 一、当前锁未被任何线程持有(state=0),则以cas方式获取锁,若获取成功则设置exclusiveOwnerThread为当前线程,而后返回成功的结果;若cas失败,说明在获得state=0和cas获取锁之间有其余线程已经获取了锁,返回失败结果。
  • 二、若锁已经被当前线程获取(state>0,exclusiveOwnerThread为当前线程),则将锁的重入次数加1 (state+1),而后返回成功结果。由于该线程以前已经得到了锁,因此这个累加操做不用同步。
  • 三、若当前锁已经被其余线程持有(state>0,exclusiveOwnerThread不为当前线程),则直接返回失败结果

由于咱们用state来统计锁被线程重入的次数,因此当前线程尝试获取锁的操做是否成功能够简化为:state值是否成功累加1,是则尝试获取锁成功,不然尝试获取锁失败。

其实这里还能够思考一个问题:

nonfairTryAcquire已经实现了一个囊括全部可能状况的尝试获取锁的方式,为什么在刚进入lock方法时还要经过compareAndSetState(0, 1)去获取锁,毕竟后者只有在锁未被任何线程持有时才能执行成功,咱们彻底能够把compareAndSetState(0, 1)去掉,对最后的结果不会有任何影响。

这种在进行通用逻辑处理以前针对某些特殊状况提早进行处理的方式在后面还会看到,一个直观的想法就是它能提高性能,而代价是牺牲必定的代码简洁性。

退回到上层的acquire方法,

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&  //当前线程尝试获取锁,若获取成功返回true,不然false
        //只有当前线程获取锁失败才会执行者这部分代码
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))  
        selfInterrupt();
}

tryAcquire(arg) 返回成功,则说明当前线程成功获取了锁(第一次获取或者重入),由取反和&&可知,整个流程到这结束,只有当前线程获取锁失败才会执行后面的判断。先来看 addWaiter(Node.EXCLUSIVE) 部分,这部分代码描述了当线程获取锁失败时如何安全的加入同步等待队列。这部分代码能够说是整个加锁流程源码的精华,充分体现了并发编程的艺术性。

获取锁失败的线程如何安全的加入同步队列 addWaiter()

addWaiter函数完成的功能是将调用此方法的线程封装成为一个结点并放入Sync queue的尾部。

private Node addWaiter(Node mode) {
    //首先建立一个新节点,并将当前线程实例封装在内部,mode这里为null.
    // mode有两种:EXCLUSIVE(独占)和SHARED(共享),默认是独占模式
    Node node = new Node(Thread.currentThread(), mode);
    // 尝试快速方式将当前node结点直接放到队尾
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 尾结点为空(即尚未被初始化过),或者是compareAndSetTail操做失败,则入队列
    enq(node);
    return node;
}

首先建立了一个新节点,并将当前线程实例封装在其内部,以后咱们直接看enq(node)方法就能够了,中间这部分逻辑在enq(node)中都有,之因此加上这部分“重复代码”和尝试获取锁时的“重复代码”同样,对某些特殊状况
进行提早处理,牺牲必定的代码可读性换取性能提高。

private Node enq(final Node node) {
    for (;;) {
        Node t = tail; //t指向当前队列的最后一个节点,队列为空则为null
        if (t == null) { // 队列为空,建立一个空的标志结点做为head结点,并将tail也指向它
            if (compareAndSetHead(new Node())) //构造新结点,CAS方式设置为队列首元素,当head==null时更新成功
                tail = head;//尾指针指向首结点
        } else {  //队列不为空
            node.prev = t;
            if (compareAndSetTail(t, node)) { //CAS将尾指针指向当前结点,当t(原来的尾指针)==tail(当前真实的尾指针)时执行成功
                t.next = node;    //原尾结点的next指针指向当前结点
                return t;
            }
        }
    }
}

这里有两个CAS操做:

  • compareAndSetHead(new Node()),CAS方式更新head指针,仅当原值为null时更新成功
/**
 * CAS head field. Used only by enq.
 */
private final boolean compareAndSetHead(Node update) {
    return unsafe.compareAndSwapObject(this, headOffset, null, update);
}
  • compareAndSetTail(t, node),CAS方式更新tial指针,仅当原值为t时更新成功
/**
 * CAS tail field. Used only by enq.
 */
private final boolean compareAndSetTail(Node expect, Node update) {
    return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}

外层的for循环保证了全部获取锁失败的线程通过失败重试后最后都能加入同步队列。

由于AQS的同步队列是不带哨兵结点的,故当队列为空时要进行特殊处理,这部分在if分句中。注意当前线程所在的结点不能直接插入空队列,由于阻塞的线程是由前驱结点进行唤醒的。故先要插入一个结点做为队列首元素,当锁释放时由它来唤醒后面被阻塞的线程,从逻辑上这个队列首元素也能够表示当前正获取锁的线程,虽然并不必定真实持有其线程实例。

首先经过new Node()建立一个空结点,而后以CAS方式让头指针指向该结点(该结点并不是当前线程所在的结点),若该操做成功,则将尾指针也指向该结点。这部分的操做流程能够用下图表示

当队列不为空,则执行通用的入队逻辑,这部分在else分句中

else {
            node.prev = t;//step1:待插入结点pre指针指向原尾结点
            if (compareAndSetTail(t, node)) { step2:CAS方式更改尾指针
                t.next = node; //原尾结点next指针指向新的结点
                return t;
            }
        }

首先当前线程所在的结点的前向指针pre指向当前线程认为的尾结点,源码中用t表示。而后以CAS的方式将尾指针指向当前结点,该操做仅当tail=t,即尾指针在进行CAS前未改变时成功。若CAS执行成功,则将原尾结点的后向指针next指向新的尾结点。整个过程以下图所示

整个入队的过程并不复杂,是典型的CAS加失败重试的乐观锁策略。其中只有更新头指针和更新尾指针这两步进行了CAS同步,能够预见高并发场景下性能是很是好的。可是本着质疑精神咱们不由会思考下这么作真的线程安全吗?

  • 1.队列为空的状况:
    由于队列为空,故head=tail=null,假设线程执行2成功,则在其执行3以前,由于tail=null,其余进入该方法的线程由于head不为null将在2处不停的失败,因此3即便没有同步也不会有线程安全问题。
  • 2.队列不为空的状况:
    假设线程执行5成功,则此时4的操做必然也是正确的(当前结点的prev指针确实指向了队列尾结点,换句话说tail指针没有改变,如若否则5必然执行失败),又由于4执行成功,当前节点在队列中的次序已经肯定了,因此6什么时候执行对线程安全不会有任何影响,好比下面这种状况

为了确保真的理解了它,能够思考这个问题:把enq方法图中的4放到5以后,整个入队的过程还线程安全吗?

到这为止,获取锁失败的线程加入同步队列的逻辑就结束了。可是线程加入同步队列后会作什么咱们并不清楚,这部分在acquireQueued方法中

线程加入同步队列后会作什么 acquireQueued()

此时的状态是:

该线程获取资源失败,已经被放入等待队列尾部了。

下面就是:

线程在等待队列中获取资源,一直获取到资源后才返回。若是在整个等待过程当中被中断过,则返回true,不然返回false。

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        // 死循环,正常状况下线程只有得到锁才能跳出循环
        for (;;) {
            final Node p = node.predecessor();// 得到当前线程所在结点的前驱结点
            // 第一个if分句
            if (p == head && tryAcquire(arg)) { 
                setHead(node); // 将当前结点设置为队列头结点
                p.next = null; // help GC
                failed = false;
                return interrupted;// 正常状况下死循环惟一的出口
            }
            // 第二个if分句
            if (shouldParkAfterFailedAcquire(p, node) &&  // 判断是否要阻塞当前线程
                parkAndCheckInterrupt())      // 阻塞当前线程
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

这段代码主要的内容都在for循环中,这是一个死循环,主要有两个if分句构成。

第一个if分句中,当前线程首先会判断前驱结点是不是头结点,若是是则尝试获取锁,获取锁成功则会设置当前结点为头结点(更新头指针)。为何必须前驱结点为头结点才尝试去获取锁?由于头结点表示当前正占有锁的线程,正常状况下该线程释放锁后会通知后面结点中阻塞的线程,阻塞线程被唤醒后去获取锁,这是咱们但愿看到的。然而还有一种状况,就是前驱结点取消了等待,此时当前线程也会被唤醒,这时候就不该该去获取锁,而是往前回溯一直找到一个没有取消等待的结点,而后将自身链接在它后面。一旦咱们成功获取了锁并成功将自身设置为头结点,就会跳出for循环。不然就会执行第二个if分句:确保前驱结点的状态为SIGNAL,而后阻塞当前线程。

先来看shouldParkAfterFailedAcquire(p, node),从方法名上咱们能够大概猜出这是判断是否要阻塞当前线程的,方法内容以下

/**
 * Checks and updates status for a node that failed to acquire.
 * Returns true if thread should block. This is the main signal
 * control in all acquire loops.  Requires that pred == node.prev.
 *
 * @param pred node's predecessor holding status
 * @param node the node
 * @return {@code true} if thread should block
 */
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL) //状态为SIGNAL

        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
    if (ws > 0) { //状态为CANCELLED,
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else { //状态为初始化状态(ReentrentLock语境下)
        /*
         * 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;
}

能够看到针对前驱结点pred的状态会进行不一样的处理

  • 1.pred状态为SIGNAL,则返回true,表示要阻塞当前线程。
  • 2.pred状态为CANCELLED,则一直往队列头部回溯直到找到一个状态不为CANCELLED的结点,将当前节点node挂在这个结点的后面。
  • 3.pred的状态为初始化状态,此时经过compareAndSetWaitStatus(pred, ws, Node.SIGNAL)方法将pred的状态改成SIGNAL。

其实这个方法的含义很简单,就是确保当前结点的前驱结点的状态为SIGNAL,SIGNAL意味着线程释放锁后会唤醒后面阻塞的线程。毕竟,只有确保可以被唤醒,当前线程才能放心的阻塞。

可是要注意只有在前驱结点已是SIGNAL状态后才会执行后面的方法当即阻塞,对应上面的第一种状况。其余两种状况则由于返回false而从新执行一遍
for循环。这种延迟阻塞其实也是一种高并发场景下的优化,试想我若是在从新执行循环的时候成功获取了锁,是否是线程阻塞唤醒的开销就省了呢?

最后咱们来看看阻塞线程的方法parkAndCheckInterrupt

shouldParkAfterFailedAcquire返回true表示应该阻塞当前线程,则会执行parkAndCheckInterrupt方法,这个方法比较简单,底层调用了LockSupport来阻塞当前线程,源码以下:

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

该方法内部经过调用LockSupport的park方法来阻塞当前线程,不清楚LockSupport的能够看看这里。LockSupport功能简介及原理浅析

下面经过一张流程图来讲明线程从加入同步队列到成功获取锁的过程

归纳的说,线程在同步队列中会尝试获取锁,失败则被阻塞,被唤醒后会不停的重复这个过程,直到线程真正持有了锁,并将自身结点置于队列头部。

非公平加锁流程源码总结

ReentrantLock非公平模式下的加锁流程以下

非公平模式解锁流程

解锁流程源码解读

解锁的源码相对简单,源码以下:

public void unlock() {
    sync.release(1);  
}
public final boolean release(int arg) {
    if (tryRelease(arg)) { //释放锁(state-1),若释放后锁可被其余线程获取(state=0),返回true
        Node h = head;
        //当前队列不为空且头结点状态不为初始化状态(0)   
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);  //唤醒同步队列中被阻塞的线程
        return true;
    }
    return false;
}

正确找到sync的实现类,找到真正的入口方法,主要内容都在一个if语句中,先看下判断条件tryRelease方法

protected final boolean tryRelease(int releases) {
    int c = getState() - releases; //计算待更新的state值
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) { //待更新的state值为0,说明持有锁的线程未重入,一旦释放锁其余线程将能获取
        free = true; 
        setExclusiveOwnerThread(null);//清除锁的持有线程标记
    }
    setState(c);//更新state值
    return free;
}

tryRelease其实只是将线程持有锁的次数减1,即将state值减1,若减小后线程将彻底释放锁(state值为0),则该方法将返回true,不然返回false。因为执行该方法的线程必然持有锁,故该方法不须要任何同步操做。
若当前线程已经彻底释放锁,即锁可被其余线程使用,则还应该唤醒后续等待线程。不过在此以前须要进行两个条件的判断:

  • h!=null是为了防止队列为空,即没有任何线程处于等待队列中,那么也就不须要进行唤醒的操做
  • h.waitStatus != 0是为了防止队列中虽有线程,但该线程还未阻塞,由前面的分析知,线程在阻塞本身前必须设置前驱结点的状态为SIGNAL,不然它不会阻塞本身。

接下来就是唤醒线程的操做,unparkSuccessor(h)源码以下

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

通常状况下只要唤醒后继结点的线程就好了,可是后继结点可能已经取消等待,因此从队列尾部往前回溯,找到离头结点最近的正常结点,并唤醒其线程。

解锁流程源码总结

公平锁加锁流程

首先是 ReentrantLock 的入口

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

其公平锁的实现以下:

final void lock() {
    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();
}

回忆一下,tryAcquire是AQS中定义的钩子方法:

protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

ReentrantLock在公平和非公平模式下对此有不一样实现,非公平模式上面已经介绍过了,公平模式的实现以下:

/**
 * 获取公平锁的方法
 *
 * 1)获取锁数量c
 *    1.1)若是c==0,若是当前线程是等待队列中的头节点,使用CAS将state(锁数量)从0设置为1,若是设置成功,当前线程独占锁-->请求成功
 *    1.2)若是c!=0,判断当前的线程是否是就是当下独占锁的线程,若是是,就将当前的锁数量状态值+1(这也就是可重入锁的名称的来源)-->请求成功
 *    最后,请求失败后,将当前线程链入队尾并挂起,以后等待被唤醒。
 */
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState(); // 获取锁数量c
    if (c == 0) { 
        if (!hasQueuedPredecessors() &&         // 若是当前线程是等待队列中的头节点
            compareAndSetState(0, acquires)) {  // 使用CAS将state(锁数量)从0设置为1
            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;
}

下边的代码与非公平锁同样。

公平锁相比非公平锁的不一样

公平锁模式下,对锁的获取有严格的条件限制。在同步队列有线程等待的状况下,全部线程在获取锁前必须先加入同步队列。队列中的线程按加入队列的前后次序得到锁。
从公平锁加锁的入口开始,

对比非公平锁,少了非重入式获取锁的方法(即少了CAS尝试将state从0设为1,进而得到锁的过程),这是第一个不一样点。

接着看获取锁的通用方法tryAcquire(),多了须要判断当前线程是否在等待队列首部的逻辑(实际上就是少了再次插队的过程,可是CAS获取仍是有的)

在真正CAS获取锁以前加了判断,内容以下

public final boolean hasQueuedPredecessors() {
    // The correctness of this depends on head being initialized
    // before tail and on head.next being accurate if the current
    // thread is first in queue.
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

从方法名咱们就可知道这是判断队列中是否有优先级更高的等待线程,队列中哪一个线程优先级最高?因为头结点是当前获取锁的线程,队列中的第二个结点表明的线程优先级最高。
那么咱们只要判断队列中第二个结点是否存在以及这个结点是否表明当前线程就好了。这里分了两种状况进行探讨:

  1. 第二个结点已经彻底插入,可是这个结点是否就是当前线程所在结点还未知,因此经过s.thread != Thread.currentThread()进行判断,若是为true,说明第二个结点表明其余线程。
  2. 第二个结点并未彻底插入,咱们知道结点入队一共分三步:
  • 1.待插入结点的pre指针指向原尾结点
  • 2.CAS更新尾指针
  • 3.原尾结点的next指针指向新插入结点

因此(s = h.next) == null 就是用来判断2刚执行成功但还未执行3这种状况的。这种状况第二个结点必然属于其余线程。
以上两种状况都会使该方法返回true,即当前有优先级更高的线程在队列中等待,那么当前线程将不会执行CAS操做去获取锁,保证了线程获取锁的顺序与加入同步队列的顺序一致,很好的保证了公平性,但也增长了获取锁的成本。

一些疑问的解答

为何基于FIFO的同步队列能够实现非公平锁?

由FIFO队列的特性知,先加入同步队列等待的线程会比后加入的线程更靠近队列的头部,那么它将比后者更早的被唤醒,它也就能更早的获得锁。从这个意义上,对于在同步队列中等待的线程而言,它们得到锁的顺序和加入同步队列的顺序一致,这显然是一种公平模式。然而,线程并不是只有在加入队列后才有机会得到锁,哪怕同步队列中已有线程在等待,非公平锁的不公平之处就在于此。回看下非公平锁的加锁流程,线程在进入同步队列等待以前有两次抢占锁的机会:

  • 第一次是非重入式的获取锁,只有在当前锁未被任何线程占有(包括自身)时才能成功;
  • 第二次是在进入同步队列前,包含全部状况的获取锁的方式。

只有这两次获取锁都失败后,线程才会构造结点并加入同步队列等待。而线程释放锁时是先释放锁(修改state值),而后才唤醒后继结点的线程的。试想下这种状况,线程A已经释放锁,但还没来得及唤醒后继线程C,而这时另外一个线程B恰好尝试获取锁,此时锁刚好不被任何线程持有,它将成功获取锁而不用加入队列等待。线程C被唤醒尝试获取锁,而此时锁已经被线程B抢占,故而其获取失败并继续在队列中等待。整个过程以下图所示

若是以线程第一次尝试获取锁到最后成功获取锁的次序来看,非公平锁确实很不公平。由于在队列中等待好久的线程相比还未进入队列等待的线程并无优先权,甚至竞争也处于劣势:在队列中的线程要等待其余线程唤醒,在获取锁以前还要检查前驱结点是否为头结点。在锁竞争激烈的状况下,在队列中等待的线程可能迟迟竞争不到锁。这也就非公平在高并发状况下会出现的饥饿问题。那咱们再开发中为何大多使用会致使饥饿的非公平锁?很简单,由于它性能好啊。

为何非公平锁性能好

非公平锁对锁的竞争是抢占式的(队列中线程除外),线程在进入等待队列前能够进行两次尝试,这大大增长了获取锁的机会。这种好处体如今两个方面:

  • 1.线程没必要加入等待队列就能够得到锁,不只免去了构造结点并加入队列的繁琐操做,同时也节省了线程阻塞唤醒的开销,线程阻塞和唤醒涉及到线程上下文的切换和操做系统的系统调用,是很是耗时的。在高并发状况下,若是线程持有锁的时间很是短,短到线程入队阻塞的过程超过线程持有并释放锁的时间开销,那么这种抢占式特性对并发性能的提高会更加明显。
  • 2.减小CAS竞争。若是线程必需要加入阻塞队列才能获取锁,那入队时CAS竞争将变得异常激烈,CAS操做虽然不会致使失败线程挂起,但不断失败重试致使的对CPU的浪费也不能忽视。除此以外,加锁流程中至少有两处经过将某些特殊状况提早来减小CAS操做的竞争,增长并发状况下的性能。一处就是获取锁时将非重入的状况提早,以下图所示

另外一处就是入队的操做,将同步队列非空的状况提早处理

这两部分的代码在以后的通用逻辑处理中都有,很显然属于重复代码,但由于避免了执行无心义的流程代码,好比for循环,获取同步状态等,高并发场景下也能减小CAS竞争失败的可能。

阅读源码的收获

  • 1.熟悉了ReentrantLock的内部构造以及加锁和解锁的流程,理解了非公平锁和公平锁实现的本质区别以及为什么前者相比后者有更好的性能。以此为基础,咱们能够更好的使用ReentrantLock。
  • 2.经过对部分实现细节的学习,了解了如何以CAS算法构建无锁的同步队列,咱们能够借鉴并以此来构建本身的无锁的并发容器。
相关文章
相关标签/搜索