ReentrantLock
这一节聊一下ReentrantLock的源码,这个也是面试中必不可少的内容,希望本文能让你有收获。
一、锁的分类
首先ReentrantLock分为公平锁和非公平锁两种,默认情况下创建的是非公平锁。在ReentrantLock的内部是存在一个Sync的内部类,公平锁和非公平锁都是这个类的子类。
/** * Creates an instance of {@code ReentrantLock}. * This is equivalent to using {@code ReentrantLock(false)}. */ public ReentrantLock() { sync = new NonfairSync(); } /** * 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(); }
根据布尔类型的构造参数来返回两个不同的类对象,我们先看一下Sync类的内容
abstract static class Sync extends AbstractQueuedSynchronizer {
这里可以看到Sync类是AQS的一个子类,AQS的就先不单独看了,因为在后面介绍一些方法的时候会涉及到其中的源码,到时候会拿出来详细的说。
二、获取锁
创建完ReentrantLock对象之后,我们最先要使用的就是lock方法,那就从这个方法开始说起。
public void lock() { sync.lock(); }
这是在ReentrantLock中的方法,可以看到是调用的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 { private static final long serialVersionUID = -5179523762034025860L; /** * Performs {@link Lock#lock}. The main reason for subclassing * is to allow fast path for nonfair version. */ abstract void lock();
所以追到这里我们可以看到在父类Sync中定义了一个抽象方法lock,所以接下来我们来对比的看一下在公平锁和非公平锁中的lock方法的实现有什么不同
非公平锁中:
final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }
公平锁中
final void lock() { acquire(1); }
这里可以很清楚的看到,在非公平锁场景下,直接尝试通过CAS的方式获取锁,在获取失败的时候再调用acquire方法,那么就来看一下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(); }
这里要说明一下,这个acquire方法是在AQS中提供的方法,跟锁的类型没有关系。这里的逻辑比较重要,因为在if条件中的逻辑比较多,那么我们先看一下tryAcquire方法
protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); }
同样是在AQS中,可以看到是一个需要子类来实现的方法,那么就还是分别看一下:
非公平锁:
/** * 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(); 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; }
公平锁:
/** * 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) { 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表示当前没有线程持有这个锁,所以会尝试获取,如果不是0,那么检查是不是重入行为,如果都不是返回false,表示尝试获取锁失败。但是因为之前的if条件判断是取反操作所以:
- 如果获取锁,或者重入锁成功,返回true ,那么根绝逻辑短路操作,后面的操作不再执行,当前线程获取锁成功
- 如果获取锁,或者重入锁失败,返回false, 那么需要执行后面的 acquireQueued 方法进行逻辑处理,这个方法下面会提到
说完了相同的部分,我们再来看一下这个两个方法中的不同部分,就是公平锁中的 hasQueuedPredcessors方法。
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()); }
我们知道公平锁的特点就是只要之前有线程比自己早申请获取锁了,那么就一定会等待之前的线程执行完,或者取消等待。所以基于以上铺垫,我们来看一下这个方法。方法的名称就是看是否有排队的前置进程,简单的说就是是否存在已经等待的任务。
而返回值是boolean,所以会存在两种结果:
- 返回True:存在前置等待任务,所以需要等待
- 返回False:当前没有前置任务在等待,所以可以尝试获取锁
所以这里根据整体的返回结果来分别说明这个方法中的几种情况:
- 返回False
- h != t 的结果为false,直接短路,整体结果返回false
- 这里h和t代表头和尾节点,如果相等可以存在两种情况,首先是两者皆为null,就是在还未初始化,那么此时肯定没有前置等待任务。
- 第二种情况就是两者不为null,那么如果两者相等,那就是两者指向同一个对象,也就是在刚完成初始化,这里我们先看一下AQS中的enq方法,会创建一个默认的Node对象,然后head和tail都指向这个对象,但是此对象并不是等待任务,只是作为一个标识对象。这么说第二个进来的任务也不需要排队,因为前置节点是head,第三个进来的线程才需要排队。
- h != t 的结果为false,直接短路,整体结果返回false
1 /** 2 * Inserts node into queue, initializing if necessary. See picture above. 3 * @param node the node to insert 4 * @return node's predecessor 5 */ 6 private Node enq(final Node node) { 7 for (;;) { 8 Node t = tail; 9 if (t == null) { // Must initialize 10 if (compareAndSetHead(new Node())) 11 tail = head; 12 } else {
-
- h != t 的结果为true , 但是 ((s = h.next) == null || s.thread != Thread.currentThread()) 的结果为false,也就是这两个条件都是false
- (s = h.next) == null 为false ,也就是说头节点的后续节点不为null,存在一个生效的等待任务
- s.thread != Thread.currentThread() 为false,表明head的后置节点就是当前线程,那么也就是说当前线程已经获取了锁,现在正在执行重入操作,所以允许
- h != t 的结果为true , 但是 ((s = h.next) == null || s.thread != Thread.currentThread()) 的结果为false,也就是这两个条件都是false
- 返回True:
- h != t 为true , (s = h.next) == null 为true
- 这种情况看起来有点疑惑,就是首先h != t 为true,那么就是当前队列中存在多个等待节点,但是为什么head的第一个后置节点又是null呢?还是在enq方法中,这里就不粘贴了,直接看上面的内容,就是在10行和11行直接中间,其中一个线程先执行了if条件里面的内容,将head初始化为一个空的Node节点,但是此时11行的代码还没有执行,也就是说有一个线程比当前线程早一步执行,那么当前线程就一定需要等待,所以这里整体会返回True
- h != t 为 true , s.thread != Thread.currentThread() 为true ,
- 这个就是比较常规的情况,h != t说明当前存在多个线程节点在等待,而且head的后置任务节点并不是当前线程,所以当前线程就一定需要等待。
- h != t 为true , (s = h.next) == null 为true
好了,这个方法深入的有点多,咱们回过头来继续看。上面提到的都是tryAcquire() 方法。那么如果获取锁成功,那么就直接返回true,通过短路操作后面的逻辑就不再执行了。但是如果获取锁失败,这个返回的是false呢?那我们继续看一下后面的逻辑: acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
首先看一下addWaiter方法:
/** * Creates and enqueues node for current thread and given mode. * * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared * @return the new node */ 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; }
这个也是AQS中的一个方法,同样与锁的类型无关。通过方法名称我们可以知道,是在同步等待队列的尾部添加当前线程,表示进入队列等待。那么这里首先获取了tail节点,如果不是null,那么就说明之前已经完成了初始化操作,通过链表指针操作完成连接,最后通过CAS条件tail节点的指针即可,但如果tail为空呢?那就需要直接进入到enq方法中了,这个方法上面简单的看过,这里咱们把整个方法都说一下:
/** * Inserts node into queue, initializing if necessary. See picture above. * @param node the node to insert * @return node's predecessor */ 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为null,那么执行初始化操作,将head和tail都指向默认的Node节点。但是这里并没有返回,所以通过自旋的方式还会再次进去循环体中,这次tail节点就不是空了,将node添加到链表的尾部,返回tail节点。
再来看一下 acquireQueued 方法:
1 /** 2 * Acquires in exclusive uninterruptible mode for thread already in 3 * queue. Used by condition wait methods as well as acquire. 4 * 5 * @param node the node 6 * @param arg the acquire argument 7 * @return {@code true} if interrupted while waiting 8 */ 9 final boolean acquireQueued(final Node node, int arg) { 10 boolean failed = true; 11 try { 12 boolean interrupted = false; 13 for (;;) { 14 final Node p = node.predecessor(); 15 if (p == head && tryAcquire(arg)) { 16 setHead(node); 17 p.next = null; // help GC 18 failed = false; 19 return interrupted; 20 } 21 if (shouldParkAfterFailedAcquire(p, node) && 22 parkAndCheckInterrupt()) 23 interrupted = true; 24 } 25 } finally { 26 if (failed) 27 cancelAcquire(node); 28 } 29 }
这个方法的主体是一个自旋处理,因为是一个同步等待队列,所以只有在前面节点执行完之后,当前节点才能尝试获取锁。所以一般情况下第一个if条件是不满足条件的,这里也先跳过,我们直接看第二个if条件:shouleParkAfterFailedAcquire , 这个方法p是前置节点,node是当前线程节点,来判断当前线程是否需要阻塞等待。
1 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { 2 int ws = pred.waitStatus; 3 if (ws == Node.SIGNAL) 4 /* 5 * This node has already set status asking a release 6 * to signal it, so it can safely park. 7 */ 8 return true; 9 if (ws > 0) { 10 /* 11 * Predecessor was cancelled. Skip over predecessors and 12 * indicate retry. 13 */ 14 do { 15 node.prev = pred = pred.prev; 16 } while (pred.waitStatus > 0); 17 pred.next = node; 18 } else { 19 /* 20 * waitStatus must be 0 or PROPAGATE. Indicate that we 21 * need a signal, but don't park yet. Caller will need to 22 * retry to make sure it cannot acquire before parking. 23 */ 24 compareAndSetWaitStatus(pred, ws, Node.SIGNAL); 25 } 26 return false; 27 }
这里涉及到了Node里面的waitStatus字段,默认初始化这个字段是没有赋值的,所以默认值就是0。这里首先获取前置节点的ws,如果是SINGAL,表示需要等待前置节点的状态,那么直接返回true,表明当前线程节点需要阻塞等待。
如果ws大于0 ,这里我们根据枚举值可以知道,只有CANCELLED的值是大于0的,所以在循环体中是通过循环不断的获取起前置节点,就是跳过已经取消状态的线程节点。通过指针的处理,最终这些取消了等待获取锁的线程节点最终是会被GC回收掉。
最终通过CAS的方式将当前线程节点的前置节点的状态设置为SINGAL,那么当前线程在下次循环的时候就会进入到等待状态。执行到这里最大可能就是当前节点刚被添加到队尾,其前置节点还没有来的及更新其ws。
private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted(); }
接下来就是 parkAndCheckInterrupt 方法,因为在执行shouleParkAfterFailedAcquire的时候,如果前置节点的ws是SINGAL,那么方法会返回true,接下来就是执行当前方法,在这里会通过LockSupport.park将当前线程阻塞住,然后会调用 Thread.interrupted 方法检测中断状态,如果期间有其他线程调用了当前线程的中断状态,则这个方法会返回true,并且清除中断状态。也就是说存在其他线程通过唤醒的方式,请求中断当前线程的等待状态。所以清除了当前线程的中断状态之后,继续执行后续逻辑,因为当前方法在自旋体中,所以会再次进入到if判断中:
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; }
这里p是其前置线程节点,那么p == head一定满足条件,然后尝试调用tryAcquire获取锁,这里因为其前置节点已经释放锁了,所以这里是有可能获取锁成功的。但是如果非公平锁,那么这里是有可能被其他线程抢占锁的。
那么不考虑这个条件,假设当前线程被中断唤醒之后,已经通过tryAcquire方法获取到了锁,通过setHead将head指针指向当前线程节点,然后因为p的prev已经为null , 所以这里将其next指针也置为null,那么当前已经执行完任务的节点p就会在下一次GC时被回收。然后返回interrupted标志位。
/** * Convenience method to interrupt current thread. */ static void selfInterrupt() { Thread.currentThread().interrupt(); }
如果上面的逻辑执行完,最终返回的interrupted标志位为true,那么就是执行到selfInterrupt方法中,因为刚才调用interrupted方法清除了中断标志位,那么这里再次设置中断标志,因为刚才在acquireQueued方法中是无法响应中断的。
三、释放锁
上面介绍到了获取锁时线程会在一定的条件下被阻塞住,然后等待其前置线程节点的状态,等待获取锁的机会。下面看一下unlock的实现,把全流程串起来
/** * Releases in exclusive mode. Implemented by unblocking one or * more threads if {@link #tryRelease} returns true. * This method can be used to implement method {@link Lock#unlock}. * * @param arg the release argument. This value is conveyed to * {@link #tryRelease} but is otherwise uninterpreted and * can represent anything you like. * @return the value returned from {@link #tryRelease} */ public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
这个release方法是AQS中的一个方法,所以是跟锁的类型无关的。这里看一下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; }
这里看state的值,如果当前线程并不是获取锁的线程,那么直接抛出异常。如果state的值为0,表明当前锁已经释放完成,如果一个线程执行了多次重入锁,那么这个值需要执行多次释放锁的操作才能变为0 。最终通过CAS的方式将state的状态修改为0 ,最终返回true 。
如果tryRelease返回true,表明当前线程已经释放了锁,那么会执行 if (h != null && h.waitStatus != 0) 条件判断。head节点指针指向h,也就是当前释放了锁的线程,那么这个h != null肯定为true 。此时前置节点的ws一定是SINGAL,所以整体if判断条件是一定满足的。然后会调用 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节点就是当前已经使用并且已经释放了锁的线程,首先如果当前线程的ws小于0 ,那么则有可能还是SINGAL状态,那么首先将状态置为0。然后获取下一个线程节点。
但是这里有一个小问题,就是如果当前线程节点的后续节点为null , 然后接下来通过一个for循环寻找下一个节点,但是在循环体中是从tail节点开始往前找,为什么不直接从前往后找呢?问题产生的原因就在与入队的方法中:
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; } } } }
这里是enq方法,我们看到在else中,首先将当前线程的prev指针连接上,然后再通过CAS的方式调整后续指针,但是这里是没有任何锁控制的,如果在并发的情况下,是有可能导致其中某个节点无法通过next指针找到其后续节点的。所以这里会从tail指针开始往前遍历查找。最终通过LockSupport.unpark唤醒后面的等待线程节点。
四、Condition
在实际场景中,我们使用ReentrantLock另外一个比较有力的支持就是,可以根据等待条件进行不同的队列等待。之我们使用Synorchized关键字进行线程等待,是所有等待获取锁的线程都在同一个队列中等待,最终在所有等待线程中随机选择一个唤醒,但是如果唤醒的线程需要依赖的资源并没有就绪,那么此次唤醒就没有什么价值,这个线程还是需要进入等待状态。
在这种场景下我们可以通过Lock的newCondition()方法来创建不同的等待队列
Condition condition = lock.newCondition()
下面看一下newCondition犯方法:
public class ConditionObject implements Condition, java.io.Serializable { private static final long serialVersionUID = 1173984872572414699L; /** First node of condition queue. */ private transient Node firstWaiter; /** Last node of condition queue. */ private transient Node lastWaiter; /** * Creates a new {@code ConditionObject} instance. */ public ConditionObject() { }
这里返回AQS其中的一个内部类COnditionObject对象,其实这个对象和我们的常规的Object对象锁一样,也是通过wait方法来阻塞等待,notify来唤醒等待线程,只是这里的名称不一样。
1 /** 2 * Implements interruptible condition wait. 3 * <ol> 4 * <li> If current thread is interrupted, throw InterruptedException. 5 * <li> Save lock state returned by {@link #getState}. 6 * <li> Invoke {@link #release} with saved state as argument, 7 * throwing IllegalMonitorStateException if it fails. 8 * <li> Block until signalled or interrupted. 9 * <li> Reacquire by invoking specialized version of 10 * {@link #acquire} with saved state as argument. 11 * <li> If interrupted while blocked in step 4, throw InterruptedException. 12 * </ol> 13 */ 14 public final void await() throws InterruptedException { 15 if (Thread.interrupted()) 16 throw new InterruptedException(); 17 Node node = addConditionWaiter(); 18 int savedState = fullyRelease(node); 19 int interruptMode = 0; 20 while (!isOnSyncQueue(node)) { 21 LockSupport.park(this); 22 if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) 23 break; 24 } 25 if (acquireQueued(node, savedState) && interruptMode != THROW_IE) 26 interruptMode = REINTERRUPT; 27 if (node.nextWaiter != null) // clean up if cancelled 28 unlinkCancelledWaiters(); 29 if (interruptMode != 0) 30 reportInterruptAfterWait(interruptMode); 31 }
这里的阻塞等待方法叫做await() , 整体的逻辑与ReentrantLock其中加锁的差不多,但是在ConditionObject中存在两个属性 firstWaiter和lastWaiter , 其实对应head 和 tail节点。也是通过这两个属性来维护自身的等待队列的。
private Node addConditionWaiter() { Node t = lastWaiter; // If lastWaiter is cancelled, clean out. if (t != null && t.waitStatus != Node.CONDITION) { unlinkCancelledWaiters(); t = lastWaiter; } Node node = new Node(Thread.currentThread(), Node.CONDITION); if (t == null) firstWaiter = node; else t.nextWaiter = node; lastWaiter = node; return node; }
addConditionWaiter 方法逻辑比较简单,通过nextWaiter将当前线程添加到队列尾部。下面重点说一下singl方法
/** * Removes and transfers all nodes. * @param first (non-null) the first node on condition queue */ private void doSignalAll(Node first) { lastWaiter = firstWaiter = null; do { Node next = first.nextWaiter; first.nextWaiter = null; transferForSignal(first); first = next; } while (first != null); }
其中通过firstWaiter方法获取到第一个等待的线程节点,然后调用起transferForSingal方法
/** * Transfers a node from a condition queue onto sync queue. * Returns true if successful. * @param node the node * @return true if successfully transferred (else the node was * cancelled before signal) */ final boolean transferForSignal(Node node) { /* * If cannot change waitStatus, the node has been cancelled. */ if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) return false; /* * Splice onto queue and try to set waitStatus of predecessor to * indicate that thread is (probably) waiting. If cancelled or * attempt to set waitStatus fails, wake up to resync (in which * case the waitStatus can be transiently and harmlessly wrong). */ Node p = enq(node); int ws = p.waitStatus; if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) LockSupport.unpark(node.thread); return true; }
这里可以看到,调用了singal之后不是直接唤醒对应的等待线程节点,而是将其添加到同步等待队列中,这是一个很大的不同

浙公网安备 33010602011771号