ReentrantLock源码分析

13 篇文章 2 订阅

首先看一下ReentrantLock的类图如下。ReentrantLock实现是基于AQS,其内部类Sync继承了AbstractQueuedSynchronizer,Sync有两个子类,非是FairSync(公平的同步器)和Nonfair(非公平的同步器)。ReentrantLock的锁的功能,就是依赖于FairSync和Nonfair进行实现。这里主要分析其lock和unLock的实现。
在这里插入图片描述

1 ReentrantLock创建

ReentrantLock有两个构造方法,有参的构造方法可以指定ReentrantLock使用哪种同步器,默认情况下是使用非公平的同步器。因为非公平的同步器可以提高程序的吞吐量。在后续会介绍二者的区别。

public ReentrantLock() {
    sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

2 lock的实现

先简单画一下lock的调用时序图:
在这里插入图片描述
下面就根据时序图顺序来分析lock的实现:
ReentrantLock.lock是直接调用了内部类Sync的lock,Sync是抽象类,其lock方法由子类实现,不同的子类实现不同,在实际执行中具体的子类实现在前面提到过,在创建ReentrantLock的时候可以指定,默认是NonfairSync。 这里我们分析NonfairSync的实现,部分地方,会说明二者的区别。

NonfairSync.lock具体实现如下:

//NonfairSync
final void lock() {
	//使用CAS设置将state的值设置为1,这也是获取锁的过程,只有state为0的时候才可以设置成功,设置成功,也就相当于当前线程获取锁成功。在ReentrantLock中,state来标识当前锁的状态。state = 0:锁没有被其他线程持有。state > 0,锁被其他线程持有,state的数量代表了重入的次数。
	if (compareAndSetState(0, 1))
		//当前线程获取锁成功后,将owner设置为当前线程
        setExclusiveOwnerThread(Thread.currentThread());
    else
    	//如果获取锁失败,说明当前锁已经被其他线程占用。
        acquire(1);
}

首先一开始就执行compareAndSetState,compareAndSetState就是通过CAS将AbstractQueuedSynchronizer的state设置为1。

这里说明一下state,state是AQS的锁状态标识,不同的AQS实现其含义不同,在ReentrantLock中,state代表当前锁是否已经被线程持有。state == 0,说明当前锁还没有被其他线程持有。state > 0表示锁已经被其他线程持有,state的状态标识持有锁的线程的重入次数。

所以这里一进来执行compareAndSetState(0, 1),也就是获取锁的过程。只有当state为0,也就是当前锁没有被其他线程持有的情况下,才会替换成功。如果替换成功,说明获取锁成功,如果获取锁成功了,就将exclusiveOwnerThread设置为当前线程,用于记录持有当前锁的线程。如果替换失败了,说明当前锁已经被其他线程持有了,此时就调用acquire。

这里需要说明的是,这就是非公平锁的非公平所在,就是说即使现在等待队列中已经有了等待的线程,但是后来的线程,依旧可以去抢占锁,只要抢到锁,后来的线程就可以优先于等待队列中的线程执行。

//AbstractQueuedSynchronizer
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        //acquireQueued返回的是线程是否中断过,因为即使线程中断过,也只有获得了锁才能继续执行。
        //如果再阻塞队列中中断过,则再线程获取锁之后,再响应中断,所以这里会触发中断。
        selfInterrupt();
}

acquire中执行主要分为三部分,先大致说明一下,再分步骤详细讲解:

  1. 再次调用tryAcquire去获取锁。tryAcquire获取锁成功,则直接执行selfInterrupt返回,selfInterrupt在后面讲解。
  2. tryAcquire获取锁失败,则调用addWaiter将当前线程加入等待队列,并返回当前线程节点。
  3. 调用acquireQueued将队列中的当前线程挂起。

tryAcquire
tryAcquire是由子类去实现的,这里看看NonfairSync的实现:

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

NonfairSync中直接调用的是父类Sync的nonfairTryAcquire:

final boolean nonfairTryAcquire(int acquires) {
	//拿到当前线程
    final Thread current = Thread.currentThread();
    //拿到当前的state
    int c = getState();
    //只有当c == 0的情况下,才说明当前锁没有被占用,才进行CAS尝试替换。这里提前判断,为了提升性能,防止每次都进行CAS操作
    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;
}
  1. 获取当前的state,并判断state是否等于0。前面说过,state == 0表示当前锁还没有被线程持有。

  2. 如果当前线程没有被持有,就去通过CAS尝试获取锁,这里获取的时候可能由于此时正有其他线程提前抢占了锁而失败。 这里就算不先判断c==0的判断,直接CAS也是可以的,但是加了c==0 ,减少了很多的CAS操作,可以提升程序的性能。
    这里在入队前进行了抢占锁的动作,这和前面说的类似,也就是非公平所的非公平的体现。

  3. 当发现锁已经被线程持有了,可能是锁重入,所以通过 current==getExclusiveOwnerThread()判断是否是重入。如果是,直接给state加acquires,直接返回true。

addWaiter
如果前面的tryAcquire返回true,说明获取锁成功了,就不会再执行addWaiter了。只有获取锁失败了,才需要将当前线程加入到阻塞队列中。

private Node addWaiter(Node mode) {
	//用当前线程创建一个新的Node节点
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    //这里判断tail不等于null。说明队列中已经有等待的线程了,直接尝试将当前线程往队列末尾追加
    if (pred != null) {
    	//这里追加的时候,先将新创建的节点的pre连接到队列上。
        node.prev = pred;
        //通过CAS替换tail,替换成功,则将队列尾节点的next指向新节点,说明加入队列成功
        //如果这里替换失败,说明有其他现成因为获取锁失败了并且正在加入到队列,并且先加入成功了。
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    //队列中没有等待线程,或者加入队列失败了,都会执行enq方法
   enq(node);
   return node;
}

在addWaiter中,首先将当前线程封装成了一个Node节点,Node是AQS定义好的双向链表节点,其中有指向前后节点的指针,保存线程的字段,节点的状态,节点模式(独占/共享),具体定义可自行翻越代码,这里就不粘贴了。

这里还需要说明的是,在AQS中,有两个字段 head 和 tail,非别用于指向等待队列的头和尾节点。

将当前线程封装成Node节点后,接下来就要将当前线程加入到队列中。

首先判断了tail尾节点是否为空,如果不为空,就尝试将新节点添加到队列尾部。这里添加分了三步:

  1. 将新节点的prev连接到原队列尾部:

在这里插入图片描述
2. 通过CAS将tail替换为最新的节点,如果替换成功,队列如下:
在这里插入图片描述
3. 如果tail替换成功了,就将前一个的next指向新节点,到此,新节点才算真正加入到队列。
在这里插入图片描述
将新节点加入到队列,为什么要这样做?首先我们知道,可能会有多个线程同时获取锁失败,并且同时来执行加入队列的操作。此时肯定要保证每个节点都加入队列成功,并且要保证多线程情况下的数据安全。所以此处通过CAS修改tail的值来保证多个线程竞争的情况下,只有一个节点能够加入成功。只要有一个线程修改了tail,其他的线程想继续修改tail肯定是失败的。一旦失败了就执行enq进行加入,下面就看看enq如果保证多线程下每个节点都能够加入成功:

    private Node enq(final Node node) {
    	//这里是一个死循环,之所以使用死循环,因为在将当前线程加入队列的时候,
    	//可能会因为其他现成提前加入成功了导致CAS失败,此时会继续再次循环尝试加入,直到加入成功为止。
        for (;;) {
            Node t = tail;
            //这里首先判断如果队列中没有等待线程,就直接初始化一个Node
            //并且将head和tail都指向初始化的node。然后再执行二次循环将当前线程加入队列。
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
           		 //否则就尝试通过CAS将当前现成加入队列,如果加入失败,则继续循环尝试加入,知道成功返回为止。
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

enq中是一个死循环,循环中的代码和前面讲到的填入队列的逻辑基本是相同的。只有当线程加入成功,才返回,如果加入队列失败了,说明已经有其他线程提前加入了,则继续下一次循环,拿到最新的tail,然后继续CAS加入,直到加入成功才返回。

这里还有一个点,就是初始化,当队列中没有元素的时候,也就是hea == tail == null的时候,就需要先初始化一个空的节点,作为头结点。所以等待队列其实是一个有头结点的双向队列:

在这里插入图片描述
讲解到这里,addWaiter就执行完了,此时新的节点就已经成功加入到队列的,addWaiter将返回新加人的节点Node。紧接着将执行acquireQueued将节点线程挂起。

//执行到这个方法,说明当前线程已经加入到了等待队列中。
//这个方法要做的事情,就是将当前线程挂起。
final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            //这里是一个死循环,只有获得了锁的线程,才能退出。
            //挂起的线程被唤醒后,会重新进行锁的抢占,抢占失败,则继续被挂起.再非公平锁中,新来的线程可以抢占锁资源。
            //被挂起的线程可能被中断唤醒。中断唤醒后,因为不能获取锁。所以会再次被挂起。
            for (;;) {
            	//获取当前线程节点的前一个节点
                final Node p = node.predecessor();
                //这里p == head,为了保证线程节点从队列上的唤醒顺序必须是从前到后按顺序依次释放
                if (p == head && tryAcquire(arg)) {
                	//执行到这里,有两种情况:
                	//1、在挂起前尝试获取锁成功了。
                	//2、锁被释放,从队列上将线程唤醒后,线程获取锁成功。
                	//如果当前线程获取了锁,则将当前线程节点从队列中移除。
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //这里开始真正挂起当前线程,先通过shouldParkAfterFailedAcquire判断是否可以挂起线程,再通过parkAndCheckInterrupt挂起当前线程
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    //parkAndCheckInterrupt返回值是是否中断唤醒,如果是,则interrupted赋值为true,当当前线程获取锁的时候,需要返回interrupted,并且再外层处理中断
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

acquireQueued的方法中,是一个死循环。先看循环中的逻辑,之后就会明白这个循环的作用。
首先拿到node的前一个节点,然后判断前一个节点p是否等于head头结点。如果等于,则通过tryAcquire获取锁。p==head的判断,只要是加入了队列中的线程,只能从头到尾按照顺序唤醒。

如果是按照我们前面的流程一步步走到这里,如果判断p==head为true,说明当前节点虽然被加入了,但是当前节点是第一个等待的节点,则在挂起前再次尝试去获取锁,如果获取成功了,则执行setHead。setHead很简单,就是将第一个节点变成头结点(也就是将获取锁的线程从队列中去除)。然后将原头结点从链表中删除(p.next = null).

private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}

如果此时node节点不是第一个节点,则会往下执行,执行真正挂起的操作。这里为什么会出现不是第一个节点来获取锁呢,一种情况就是新加入的节点,另一个情况和中断相关,我们后面再详细说明。这里先看真正挂起的逻辑。

在节点真正挂起之前,先调用shouldParkAfterFailedAcquire判断了是否应该挂起。具体逻辑如下:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
	//获取当前节点的前一个节点的状态,通过前一个节点的waitStatus判断是否挂起当前线程
    int ws = pred.waitStatus;
    //如果前一个节点的waitStatus == GIGNAL,则挂起
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
    //如果前一个节点的waitStatus > 0,即waitStatus == CANCELLED
    //此时从后往前遍历,将所有waitStatus == CANCELLED的节点都从等待队列上移除,知道前一个节点 waitStatus <= 0为止
    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.
         */
         //如果waitStatus == 0,则将其状态设置为SIGNAL
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    //当waitStatus >= 0的时候,这里都会返回false,退出后因为返回false,所以并不会挂起当前线程。调用这个方法的地方是死循环,所以再下次循环的时候会继续调用此方法判断是否可以挂起线程。
    return false;
}

这里的逻辑,就是判断当前要挂起节点的前一个节点的waitStatus是否等于SIGNAL(值为 -1 ),如果是,才将当前节点挂起。如果不是,则继续判断前一个节点的waitStatus是否大于0,从Node的定义中可以看到,waitstatus大于0的值只有一个,就是CANCELLED(值为1),这种状态说明当前线程节点已经被取消执行,可以理解为不需要再唤醒此线程了,那就说明这种节点可以直接从队列中删除了。当然程序也是这样做的,从当前节点往前找到第一个waitstatus不为CANCELLED,并重新连接队列,也就是将从前往后遍历过程中遇到的waitstatus为CANCELLED的节点删除掉。重组链表后,返回false。因为返回的是false,在acquireQueued中并不会将当前节点挂起,因为是死循环,所以会再次执行一次,在次执行挂起的逻辑。

shouldParkAfterFailedAcquire中,如果前一个节点的waitstatus为0,则通过CAS将其设置为SIGNAL,并返回false,重新执行挂起逻辑。

这里需要说明一下,对于节点的waitstatus,在ReentrantLock中,Node在创建的时候waitstatus默认值为0,当有新的节点入队列的时候,就会将前一个节点的waitstatus设置为SIGNAL。还记得前面说过的setHead吗,setHead中会把队列第一个节点(非首节点)重新设置为head,也就是head的值可能是SIGNAL,或者是0(队列没有等待节点了,因为最后一个节点后面没有节点,其waitstatus会是0,当把head设置为最后一个节点的时候,head的waitstatus就是0)。可以记住这里的分析,在将锁的释放(unlock)的时候,要理解有些地方的判断,需要用到这些。

如果shouldParkAfterFailedAcquire返回为true,说明前一个节点的waitstatus已经是SIGNAL了,此时开始执行真正的挂起parkAndCheckInterrupt。

private final boolean parkAndCheckInterrupt() {
	//将当前线程挂起
   LockSupport.park(this);
   //挂起后,可以因为中断将线程唤醒,所以唤醒后直接判断是否是中断唤醒,interrupted返回中断状态,并将线程的中断标志重置。
   return Thread.interrupted();
}

挂起的逻辑很简单,就是调用 LockSupport.park将当前线程挂起。一定要记住这里,当线程挂起后,线程就停留在这里不再继续往下执行了,方法不会返回。当执行unlock将线程唤醒的时候,唤醒的线程会继续从此出往下执行。

说完了整个获取锁的逻辑,现在我们再来看另一个问题,如果被挂起的线程发生了中断,怎么办呢?会立即响应中断吗?
通过前面的分析我们知道:

  1. 被挂起的线程会放在队列中,而队列中的节点只能从头到尾依次获取锁。
  2. 如果线程被中断了唤醒了,因为他依旧处于死循环中(acquireQueued方法中),所以会执行获取锁的逻辑,如果无法获取锁,即使是被中断唤醒,还是会再次被挂起。所以并不会立即响应中断。那中断什么时候响应呢?

分析源码可以看到,在parkAndCheckInterrupt通过park挂起线程之后,如果线程被唤醒了,调用的是Thread.interrupted,Thread.interrupted方法返回的是中断标志位,如果线程被中断了则为true,如果没有中断为false。并且Thread.interrupted返回中断标志位的同时还会将线程的中断标志重置为false。所以,当线程被唤醒,通过Thread.interrupted就能够判断出线程是否是被中断唤醒的,如果返回的是true,在acquireQueued中会将标量interrupted设置为true,当然此时因为是中断唤醒,这个线程可能并不会获取锁,会继续被挂起。当这个线程在后续获取到锁的时候,acquireQueued会返回interrupted,此时如果中断过,interrupted就为true。在acquire中如果acquireQueued返回值为true,说明线程在挂起的过程中被中断过,所以会响应中断,此时会调用Thread.currentThread().interrupt()将线程中断以后置响应挂起过程中的中断。对java线程中断不清楚的可以查看 java线程状态变更及中断实现

再看一个问题,上面讲的都是非公平锁,那公平锁再获取的时候是样的,和非公平所有什么区别呢?
其实公平锁和非公平锁的整个获取流程,逻辑基本都是相同的。前面说过,非公平锁的体现主要是后来的线程可以抢占锁,如果抢占到,就可以优先于队列中已经等待的线程之前执行。可以猜测:公平锁既然叫公平锁,那就一定按照先后顺序执行,如果后来的线程发现队列中有等待的线程,就不会去试图抢占锁,而是直接加入队列并挂起,那真是这样吗,可以跟一下公平锁的获取流程可以看到:

  1. FairSync的lock中并没有尝试去获取锁:
final void lock() {
            acquire(1);
        }
  1. 公平所的tryAcquire中通过hasQueuedPredecessors判断了队列中是否有等待线程,如果有则不会去执行compareAndSetState获取锁。
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;

至此验证了我们的猜想是正确的。

unlock的实现

unlock首先,调用的是AQS的release方法。release分两步,首先调用tryRelease释放锁,也就是将state做减法操作。如果释放锁成功,就从等待队列中唤醒一个线程。

public final boolean release(int arg) {
	//释放锁,即对state做减法操作,直到state == 0,表示释放成功。
    if (tryRelease(arg)) {
    	//执行到这儿,说已经释放锁,所以需要唤醒线程。
        Node h = head;
        //这里判断h.waitStatus != 0,0是默认状态。在挂起线程的时候,判断了前一个节点是否是SIGNAL == -1,如果是0,则会将其改为SIGNAL。而唤醒线程的时候,会将 h.waitStatus修改为0
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

首先看一下tryRelease:

//这个方法很简单,就是将state的状态值减去releases并重新赋值。
//当最终的state == 0返回true,标识锁释放成功。如果是重入锁,需要多次释放。
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;
}

其实现逻辑很简单,当state的值做减法操作,并重新设置state的值,这里设置state的值没有使用CAS,很显然是不需要的,因为只有获得了锁的线程才能够释放锁,而同一时刻只有一个线程能获取锁资源,所以这里不会存在多线程竞争的情况。只有当state被减为0的时候,才会返回true,并将exclusiveOwnerThread的值设置为null,否则放回false。state不等于0说明当前线程多次重入,没有完全释放锁。

如果释放锁成功了,接下来就要从队列中唤醒一个线程节点。
这里需要注意的是,执行tryRelease的时候,确实只能会是一个线程在执行,因为只有获取了锁的线程才会执行tryRelease释放锁。但是一旦tryRelease执行成功返回,说明锁已经释放成功了,释放锁的线程接下来会执行unparkSuccessor进行队列的线程唤醒,对于非公平锁来说,一旦锁被释放,新来的线程就会抢占锁并执行,那么对于执行unparkSuccessor唤醒线程的操作,就可能会使多个线程同时执行唤醒。

在唤醒前,首先判断了head节点的waitstatus状态如果是小于0,即为SIGNAL,就将其改成0。那么如果head的waitstatus等于0,就说明已经唤醒过了,所以在release还判断了当h.waitStatus != 0的情况下,才进行唤醒操作。

在unparkSuccessor还会判断,如果当前需要唤醒的节点的状态是大于0的,说明是CANCELED状态,那就没有必要唤醒这个节点,此时他会从后往前找到队列中从首到尾第一个非 CANCELED的节点唤醒。这里之所以从后往前,是因为再将节点加入队列的时候,先连接的pre,之后CAS成功后再连接next,所以pre成功连接,并不能保证节点已经加入到队列,只有next连接成功,才能说明节点成功加入队列。所以如果从前往后,取得的节点可能还没有成功连接到队列。

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.
     */
     //如果头结点的waitStatus < 0,则将其赋值为0
    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为NULL或者waitStatus > 0(取消),
     //则从后往前找到第一个waitStatus <= 0的Node,然后释放
     //这里之所以从后往前,是因为再将节点加入队列的时候,先连接的pre,之后CAS成功后再连接next
     //所以pre成功连接,并不能保证节点已经加入到队列,只有next连接成功,才能说明节点成功加入队列。
     //所以如果从前往后,取得的节点可能还没有成功连接到队列。
    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);
}
  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值