前言
在前一篇文章中我们介绍了AQS的一些基本概念,AQS作为基础组件,对于锁的实现存在两种不同的模式:独占模式(只有一个线程能执行,如ReentrantLock)和共享模式(多个线程可以同时执行,如Semaphone/CountDownLatch)。另外在这两种模式下获取锁分别提供了三种获取方式:不响应线程中断获取(acquire),响应线程中断获取(doAcquireInterruptibly),设置超时时间获取(doAcquireNanos)。接下来我们先看看acquire是怎么实现的。
1.1不响应线程中断获取
源码如下
//不响应中断方式获取 public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
从源码中可以看出,acquire内部一共调用了四个方法,分别是:
- tryAcquire()尝试获取锁,如果成功返回true。
- addWaiter()以独占模式将线程加入队列尾部。
- acquireQueued()线程在等待队列中获取资源。
- selfInterrupt()线程在等待过程中不响应中断,获取资源后进行自我中断。
接下来我们分别详细看下四个方法是如何实现的。
1.1.1tryAcquire()
此方法尝试获取资源,如果成功则返回true,否则返回false,AQS并没有实现该方法,具体获取锁的操作需要其子类比如ReentrantLock中的Sync实现:
protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread();//获取当前线程 int c = getState();//获取state值 if (c == 0) {//state为0表示当前没有线程占有锁 if (!hasQueuedPredecessors() &&//检查队列中是否有线程在当前线程的前面 compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current);//设置当前线程成功获取锁 return true; } } //state不为0,且占有锁的线程是当前线程 else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires;//增加重入次数 if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc);//更改state值 return true; } return false;//如果锁被其他线程占用,获取锁失败 }
1.1.2addWaiter()
如果获取锁失败,则将线程加入队列尾部,返回新插入的节点。源码如下
private Node addWaiter(Node mode) {//将当前线程包装成节点,并添加到队列尾部 Node node = new Node(Thread.currentThread(), mode);//以给定模式构建节点,独占或者共享 //获取队列尾节点 Node pred = tail; if (pred != null) { node.prev = pred;//将当前节点的前驱节点设置为当前尾节点 if (compareAndSetTail(pred, node)) { pred.next = node;//将旧尾节点的后继节点设置为新的尾节点 return node; } } enq(node);//队列没有初始化 return node; }
//节点入队操作 private Node enq(final Node node) { for (;;) { Node t = tail;//获取同步队列尾节点 if (t == null) { // 如果尾节点为空,则同步队列还没有初始化 if (compareAndSetHead(new Node())) tail = head;//初始化同步队列 } else { node.prev = t;//将尾节点设置为插入节点的前驱节点 if (compareAndSetTail(t, node)) {//设置当前插入节点为尾节点 t.next = node;//将旧的尾节点的后继节点指向插入节点 return t;//返回旧的尾节点 } } } }
1.1.3acquireQueued()
当线程加入同步队列后,会立马执行这个方法,该方法进行会进行自旋操作,不断尝试让该线程获取倒锁。源码如下
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)) {//如果前驱节点是head,则尝试获取锁 setHead(node);//拿到锁,将给定节点设置为头节点 p.next = null; // 将上一个head节点的后继节点置为null,方便垃圾回收 failed = false;//设置获取成功状态 return interrupted;//返回中断的状态 } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())//表明此时无法获取到锁,判断是否要挂起当前线程 interrupted = true; } } finally { if (failed) cancelAcquire(node);//如果获取失败就取消获取 } }
//判断是否可以将当前线程挂起 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus;//获取前驱节点的等待状态 if (ws == Node.SIGNAL) return true;//如果前驱节点的等待状态是signal,则表明前驱节点会唤醒当前节点,因此将当前节点挂起 if (ws > 0) {//ws>0表示节点已经被取消,该节点会被删除 do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node;//直到找到一个没有被取消的节点,排在它的后边 } else { //将前驱节点的状态设置为signal,以便于可以唤醒当前节点。 compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }
//挂起当前线程 private final boolean parkAndCheckInterrupt() { LockSupport.park(this);//调用park()使线程进入等待状态 return Thread.interrupted(); }
1.1.4selfInterrupt()
线程在获取资源后进行自我中断处理,源码如下
static void selfInterrupt() {
Thread.currentThread().interrupt();//当前线程将自己中断
}
1.1.5总结
首先通过tryAcquire尝试获取锁,如果成功则直接返回,如果失败,则调用addWaiter将该线程加入等待队列的尾部,加入队列后立马执行acquireQueued方法,不断轮询尝试获取锁,直到获取到锁返回。如果在整个等待过程中被中断过,则返回true,否则返回false。如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt()。流程图如下所示
1.2响应中断获取锁
响应中断就是当线程从从parkAndCheckInterrupt方法中醒来后会检查线程是否中断,如果是的话就抛出InterruptedException异常,而不响应线程中断获取锁是在收到中断请求后只是设置一下中断状态,并不会立马结束当前获取锁的方法,一直到结点成功获取到锁之后才会根据中断状态决定是否将自己挂起。源码如下
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();//线程被唤醒后如果有中断请求就抛出异常
}
} finally {
if (failed)
cancelAcquire(node);
}
}
1.3设置超时时间获取锁
设置超时时间获取会将传入的超时时间与自选时间进行比较,大于则将线程挂起一段时间。每次获取锁之后都会将超时时间减去获取一次锁所用的时间。一直到超时时间小于0,那么返回获取失败标志。源码如下
private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { if (nanosTimeout <= 0L) return false;//超时时间小于0,获取失败 final long deadline = System.nanoTime() + nanosTimeout;//获取当前系统时间+超时时间=过期时间 final Node node = addWaiter(Node.EXCLUSIVE);//以独占模式构建节点 boolean failed = true; try { for (;;) { final Node p = node.predecessor();//获取当前节点的前驱节点 if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return true; } nanosTimeout = deadline - System.nanoTime();更新超时时间 if (nanosTimeout <= 0L) return false;//超时时间小于0,获取失败 if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold)//spinForTimeoutThreshold=1000L LockSupport.parkNanos(this, nanosTimeout);//如果超时时间大于自旋时间,将当前线程挂起一段时间, 之后再自己醒来 if (Thread.interrupted()) throw new InterruptedException();//在获取锁的期间收到中断请求就抛出异常 } } finally { if (failed) cancelAcquire(node); } }