简单理解下ReentrantLock的加锁过程
源码版本:JDK10
首先我们知道ReentrantLock默认是非公平的和可重入的,基于AQS实现,(AQS是什么?AbstractQueuedSynchronizer:一个队列同步器,用来构建锁或者其他同步组件的基础组件,之所以叫队列同步器,是因为它使用一个由双向链表实现的队列来完成线程的排队等待,以及使用一个int变量来表示同步状态。)。因此ReentrantLock正是通过自定义的同步器Sync来完成加锁过程的。(Sync继承自AbstractQueuedSynchronizer)
ReentrantLock reLock = new ReentrantLock(); reLock.lock();
调用lock()方法后,该方法会把加锁操作交给sync来实现:
public void lock() { sync.acquire(1); }
sync是ReentrantLock内部自己实现的AQS,有两种,公平的和非公平的,取决与new ReentrantLock时的构造参数,默认不带参数或者参数为false那么sync都会是非公平的。
public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
自定义的Sync增加了nonfairTryAcquire方法,以及重写了一些其他方法。重点是nonfairTryAcquire方法,后面解释。
非公平的同步器NonfairSync继承了自定义的同步器Sync,重写了tryAcquire方法,调用了Sycn增加的nonfairTryAcquire方法。
static final class NonfairSync extends Sync { private static final long serialVersionUID = 7316153563782823691L; protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires);//该方法在Sync中实现 } }
回到刚才sync.acquire(1),该方法最主要的3个方法:
tryAquire(int arg) :实际上是调用上面Sycn的nonfairTryAcquire()
acquireQueued(final Node node,int arg) :AQS实现
addWaiter(Node mode):AQS实现
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); //如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。
}
tryAquire(int arg):刚才说了非公平的同步器NonfairSync继承了自定义的同步器Sync,重写了tryAcquire方法,调用了Sycn增加了nonfairTryAcquire方法。
因此这个方法会调用Sycn增加的nonfairTryAcquire(int acquires)方法。
final boolean nonfairTryAcquire(int acquires) { //非公平的尝试获取锁 final Thread current = Thread.currentThread(); //得到当前线程 int c = getState(); //获取同步状态,也就是state的值。ReentrantLock中state表示锁获取的次数 if (c == 0) { //同步状态为0,说明此时没有线程获取锁 if (compareAndSetState(0, acquires)) { //使用CAS的方式修改同步状态,这也是AQS底层的方法。 setExclusiveOwnerThread(current); //修改成功,这成功获得了锁,那么就把独占线程设置为自己的线程。这同样是AQS底层的方法。 return true; //获取成功 } } else if (current == getExclusiveOwnerThread()) { //如果同步状态大于0,那么说明锁已被线程获取,那么判断获取线程是否是当前线程 int nextc = c + acquires; //如果获取锁线程是当前线程,那么此时是重入,同步状态需要加一 if (nextc < 0) //如果修改后的同步状态小于0,那么说明锁获取次数超过了int的范围,一般可能是用完锁以后没有释放,一直重入,那么同步状态就可能超出int范围。 throw new Error("Maximum lock count exceeded"); setState(nextc); //更新同步状态 return true; //获取成功 } return false; //获取失败 }
addWaiter(Node mode):在获取锁失败后执行,该方法由AQS实现。
先构造节点,再把节点加入到队列中,传入的参数是Node.EXCLUSIVE,表明是独占锁
private Node addWaiter(Node mode) { Node node = new Node(mode); //new一个新的节点 for (;;) { //死循环,知道节点成功加入队列为止 Node oldTail = tail; //获得尾结点(AQS中的变量) if (oldTail != null) { //如果尾结点不为null node.setPrevRelaxed(oldTail); //把新节点的前驱节点设置为尾结点,尾插法 if (compareAndSetTail(oldTail, node)) { //使用CAS的方式把尾结点设置为新节点,更新了尾结点 oldTail.next = node; //设置成功,把旧尾结点的后继节点指向新节点,此时新节点才算入队成功 return node; //返回新节点 } } else { initializeSyncQueue(); //如尾结点为null,那么初始化队列,new一个新的节点作为尾结点。 } } }
acquireQueued(final Node node,int arg):上个节点入队方法返回的节点作为该方法的参数,arg为1。该方法同样由AQS实现
final boolean acquireQueued(final Node node, int arg) { boolean interrupted = false; //中断标记设置为false try { for (;;) { //死循环,最后返回当前线程在获取锁过程中是否曾经被中断 final Node p = node.predecessor(); //得到当前节点的前驱节点 if (p == head && tryAcquire(arg)) { //如果前驱节点是头节点并且成功获取了锁(这是刚才讲的第一个方法) setHead(node); //把当前节点设置为头结点 p.next = null; // help GC return interrupted; //返回中断标记 } if (shouldParkAfterFailedAcquire(p, node)) //这个方法主要是用来设置前继节点的状态以及拿掉等待队列中已经取消的节点。 interrupted |= parkAndCheckInterrupt(); //更新线程的中断标志。 } } catch (Throwable t) { cancelAcquire(node); if (interrupted) selfInterrupt(); throw t; } }
新创建的节点加入到等待队列以后,其实还有一个事情没有做,就是要设置前继节点的waitStatus。 尾节点的waitStatus为默认值0,因为waitStatus的意义是为了标记后继节点的状态以及行为的。
所以for循环第一次进入shouldParkAfterFailedAcquire方法的时候,前继节点的waitStatus为0,会设置成-1,当再一次进入的时候会判断该值为-1,直接返回true。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { //见名知意:当前线程应该park(阻塞)在获取锁失败后。 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) { //ws大于0,说明节点的状态是等待超时或者被中断了,需要取消。所以从尾部开始往前,直到找到第一个小于等于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. */ pred.compareAndSetWaitStatus(ws, Node.SIGNAL); //如果节点的waitStatus等于0或者PROPAGATE(传播,值为-3)
} return false; } //这里不返回true让当前节点阻塞,而是返回false,目的是让调用者再check一下当前线程是否能
成功获取锁,失败的话再阻塞。
private final boolean parkAndCheckInterrupt() { //见名知意:阻塞当前线程,并检查当前线程的中断标志(如果被中断了,则返回ture,否则返回false)。 LockSupport.park(this); //先阻塞 return Thread.interrupted(); //线程被前驱节点唤醒后,返回线程的中断标志 }
至此,三个方法基本分析完了,简单总结一下,三个方法对应三步:
第一步:首先尝试获取锁:获取同步状态,如果同步状态为0,则CAS修改,修改成功后,把独占线程设置为当前线程,返回ture,如果修改失败或者同步状态不为0且独占线程不是当前线程则执行下一步。
第二步:构造独占节点,在死循环中,使用CAS的方式把节点加入队列,如果尾结点为null,则先初始化队列。
第三步:节点加入队列后,同样在死循环中,判断节点的前驱节点是否是头节点,如果是则再次尝试获取锁,如果不是或者获取失败, 则当前线程进入阻塞状态。等待被前驱节点被唤醒。阻塞被唤醒之后如果是队首并且尝试获取锁成功就返回true,否则就继续执行前一步的代码进入阻塞。
更简单点说:
第一步:获取锁,获取成功则返回,获取失败。执行二,三步。
第二步:构造节点,用CAS的方式把节点加入到队列尾部。(CAS是为了确保线程安全)
第三步:当前线程进入阻塞状态。
锁的释放则比较简单了,同步状态-1,节点出队,唤醒后继节点。