lock方法会调用acquire方法,该方法在AQS中实现
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
正常使用一个ReentrantLock 的lock() 方法时,在不能获得所得情况下,该方法是阻塞的,对吧
以上acquire方法拆解一下方法调用
首先tryAcquire(arg),英语不好的我Acquire,百度给出的意思是获得,获取,那么好这里可以给成意思是尝试获得的意思?
如果申请资源失败,则返回false,那么会先调用addWaiter(Node.EXCLUSIVE),这里的Node.EXCLUSIVE代表独占的意思,so这个节点是独占的?
/**
* 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) { //如果当前链表有尾节点,就把封装当前线程的节点追加到尾部?这里的尾节点操作也是CAS的
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 如果没有等待的线程(节点列表为null),则初始化头尾节点,该方法是一个自旋的方法
enq(node);
return node;
}
这里考虑多线程调用的情况,这里的链表不存在线程安全问题,因为对每个头尾节点的操作都是CAS的。详见代码片段里的
compareAndSetHead和compareAndSetTail方法。
/**
* 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;
//第一次的时候尾节点肯定是一个 null,则初始话一个空内容节点为头节点,第二次循环时尾节点已经不是null了
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t; //将node节点的前驱节点设置成尾节点,即先尝试追加到链表的尾部,如果尾节点是t,则将尾节点设置成node,退出循环
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
所以第一个等待锁的线程A在AQS中的数据结构是这样的?
空节点 -> node(name:A)
看到这里,AQS中对内部链表的头尾操作都是CAS的,所以链表中的节点排序也是完全按照申请锁的顺序排列的。addWaiter方法的操作无非是将线程封装为一个node,追加该节点到链表的尾节点,然后addWaiter 方法返回封装好的node方法,继续调用acquireQueued(node) ,
acquireQueued 方法是一个自旋的方法,假设当前只有一个线程A排队申请锁,目前链表形式为。
空节点 -> node(name:A)
/**
* Acquires in exclusive uninterruptible mode for thread already in
* queue. Used by condition wait methods as well as acquire.
*
* @param node the node
* @param arg the acquire argument
* @return {@code true} if interrupted while waiting
*/
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {//自旋方法
final Node p = node.predecessor();
//获得p的前驱节点,并判断是否为head节点,如果为head节点且成功获取到资源,就将当前线程节点设置成队列的头节点,并返回
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
这里判断p==head,如果node的前驱节点是head,说明该节点排成了老二,而老二如果成功获取到资源,则变成老大,那么这里老大的起到的是什么作用?
>
我的程序有一把锁在外边阻塞中(sleep),从代码上看一开始tryAcquire方法就成功申请到资源,就不会有他的node维护在这里,就是说他的锁不需要在这里排队?那么后续的unlock是怎样的一个机制?
因为锁在别的线程中持有(state>0),所以这个acquireQueued方法中的tryAcquire会返回false
然后shouldParkAfterFailedAcquire,字面意思如果申请资源失败则判断是否可以挂起线程
/**
* 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;
//如果是SIGNAL状态,返回true,第一次来这个肯定不会成立,因为是node的前驱节点是一个空节点。
//空节点->node(A)
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) { //Node.CANCLED 如果节点是被放弃的(什么情况下会放弃,中断么??)
/*
* 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.
*/
//将前驱节点的状态设置成SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
至此针对我的测试,这个方法会走compareAndSetWaitStatus(pred, ws, Node.SIGNAL); 分支,所以目前node(name:A)节点的前驱(head节点)的waitStatus状态为 SIGNAL,该方法的第一次调用会返回false
目前针对线程A的链表表示
node(waitStatus:SIGNAL)->node(name:A)
因为shouldParkAfterFailedAcquire返回了false,所以不会调用parkAndCheckInterrupt,那么会进入第二次循环。
node的前驱节点是head 这个是毫无疑问的,但是就是因为获取锁的线程还是一个熊孩子,没有释放资源(state>0)
所以依旧会调用shouldParkAfterFailedAcquire方法,但此时方法内部已经不一样了,这里已经开始满足第一个条件分支了,所以此方法此次会返回true,因为因为shouldParkAfterFailedAcquire返回了true,所以会接着调用parkAndCheckInterrupt()方法
该方法实现如下:
/**
* Convenience method to park and then check if interrupted
*
* @return {@code true} if interrupted
*/
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);//调用park()使线程进入waiting状态
return Thread.interrupted();//如果被唤醒,查看自己是不是被中断的。
}
LockSupport.park(this); 大神说park()会让当前线程进入waiting状态。在此状态下,有两种途径可以唤醒该线程:1)被unpark();2)被interrupt()。
如果当前线程waiting了,那么就不会有第三次循环了….因为当前线程已经waiting了……
这里阻塞的线程应该是申请锁的线程,LockSupport.park(this);参数的作用是对应的blocker会记录在Thread的一个parkBlocker属性中,通过jstack命令可以非常方便的监控具体的阻塞对象.
如果此时又有一个线程B,按照此逻辑继续申请一把锁,按AQS的处理逻辑会维护成如下形式
node(waitStatus:SIGNAL)->node(name:A)->node(name:B)
假设tryAcquire(arg)依旧失败,state依旧不给释放,熊孩子还不还,那么对于node(name:B) 来说,在acquireQueued中他的前驱肯定不是head节点,所以他只能走shouldParkAfterFailedAcquire方法设置前驱节点的状态,然后调用parkAndCheckInterrupt()方法,去阻塞当前当前申请锁的线程。
同上经历2次自旋后在AQS中链表的维护成如下形式
node(waitStatus:SIGNAL)->node(name:A,waitStatus:SIGNAL)->node(name:B)
这里贴一个大神的总结和图
再来总结下它的流程吧:
调用自定义同步器的tryAcquire()尝试直接去获取资源,如果成功则直接返回;
没成功,则addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
acquireQueued()使线程在等待队列中休息,有机会时(轮到自己,会被unpark())会去尝试获取资源。获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。
由于此函数是重中之重,我再用流程图总结一下:
现在AQS的链表里有三个节点。
node(waitStatus:SIGNAL)->node(name:A,waitStatus:SIGNAL)->node(name:B)
假设一开始长时间阻塞不还锁的线程,时间到了玩够了回家吃饭去,线程中调用了reentrantlock.unlock(); 方法,他玩够了我这边两个兄弟还在那waiting呢,是不是会释放他的线程标志
释放锁的代码也是很简单的
public void unlock() {
sync.release(1);
}
这里sync是实现AQS的一个内部类Sync的实例,所以本质上还是在调用AQS的功能,看看怎么做的。
/**
* 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) //头结点不为null,且头结点是有状态的?
unparkSuccessor(h);//唤醒等待队列里的下一个线程
return true;
}
return false;
}
由上文得知,调用AQS的acquire(int args)方法,会调用子类的tryAcquire(int args)实现从而实现对线程状态的重置,那么这里有一个tryRelease(arg) 是不是也是子类的一个实现呢?其实就是调用的子类实现,依旧不去管他的实现
暂且把tryRelease当作释放资源,如果释放成功则将头节点做为参数,调用unparkSuccessor方法。
/**
* Wakes up node's successor, if one exists.
*
* @param node the node
*/
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; //拿到链表中的第二个节点,本例是node(name:A,waitstatus:SIGNAL)
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(name:A,waitstatus:SIGNAL) 节点所持有的线程将得到释放。但是这个节点并没有被移除。AQS中的链表结构变成了这样
node(waitstatus:0)->node(name:A,waitstatus:SIGNAL)->node(name:B)
此时线程A将得到唤醒,然后他会继续做final boolean acquireQueued(final Node node, int arg) 方法中的自旋。为方法继续把acquireQueued的代码拿出来再贴一遍
/**
* Acquires in exclusive uninterruptible mode for thread already in
* queue. Used by condition wait methods as well as acquire.
*
* @param node the node
* @param arg the acquire argument
* @return {@code true} if interrupted while waiting
*/
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {//自旋方法
final Node p = node.predecessor();
//获得p的前驱节点,并判断是否为head节点,如果为head节点且成功获取到资源,就将当前线程节点设置成队列的头节点,并返回
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
此时线程A发现他的前驱节点为head,并且此时再申请资源tryAcquire(arg) ,因为那个熊孩子把资源释放了,所以此时申请资源是成功的,然后会将当前节点设置为链表的头结点,并释放原来的头结点占用的内存。
这样链表结构就成了这个样子:
node(name:A,waitstatus:SIGNAL)->node(name:B)
针对线程A来说acquireQueued 方法已经执行完成,假设我们一直老老实实的等没有通知中断线程,则线程A中的代码行reentrantLock.lock() 方法将返回,一个代码行执行完成,会接着执行下一个代码行,lock() 加锁成功了!那么问题来了(问题和挖掘机没有任何关系)tryAcquire和tryRelease 到底在争抢和释放什么东西?
继续看tryAcquire方法在ReentrantLock类中的实现(公平锁)
/**
* 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();
//getState来自AQS,放方法取AQS中的全局变量state 注意是全局
int c = getState();
if (c == 0) { //AQS中默认state值为0,如果为0应该可以证明当前AQS队列里的线程没有修改过state,也就是说没有人持有锁
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;
}
可以发现tryAcquire 方法会先获取AQS中的全局变量,也可以说AQS维护的链表中的所有节点都会去检查state这个变量,所以这个state是一个线程状态,也可以看作是他们在抢的一把锁,如果发现state为0,则代表竞争到资源,并通过CAS的方式设置该值为1,然后设置当前线程为exclusiveOwnerThread,另一个条件因为锁是可重入的啊老铁
这里有一个hasQueuedPredecessors() 方法,参照某大神所说
正如hasQueuedPredecessors的注释所说,该方法的作用是为了避免 “线程闯入”,即对于ReentrantLock来说,即便是state为0时,AQS队列中也可能是有节点的(被取消的,打断的节点)等等,这个时候为了保证AQS队列的公平性,不再尝试加锁,而是返回false,到AQS的队列中去排队。所以,这也是该方法被用在公平锁中的原因。
遗留问题:
1.为什么用链表管理线程
2.节点的状态SIGNAL 主要含义
3.在AQS中的线程如何响应中断,中断策略是什么?
/** waitStatus value to indicate thread has cancelled */ static final int CANCELLED = 1; /** waitStatus value to indicate successor's thread needs unparking */ static final int SIGNAL = -1; /** waitStatus value to indicate thread is waiting on condition */ static final int CONDITION = -2; /** * waitStatus value to indicate the next acquireShared should * unconditionally propagate */ static final int PROPAGATE = -3;