(原创)多线程并发:AQS源码分析(1)——独占锁的实现原理
谈到java中的并发,我们就避不开线程之间的同步和协作问题,谈到线程同步和协作我们就不能不谈谈jdk中提供的AbstractQueuedSynchronizer(翻译过来就是抽象的队列同步器)机制;(一)、AQS中的state和Node含义:
AQS中提供了一个int volatile state状态的变量用来标识共享资源,AQS定义了两种资源的占用方式:
1、独占模式(EXCLUSIVE):表示同一个资源,在同一时刻只能被一个线程持有,例如ReentrantLock等; 2、共享模式(SHARED):表示同一个资源,在同一时刻可以被多个线程同时持有,例如Semaphore,CountDownLatch等; 同时也提供了一个LCH队列,用来存放获取共享资源时候发生阻塞的Node节点,这个节点是对需要获取资源线程的一个封装,包含了线程本身和Node节点的状态waitStatus,一共分为五种:
/**表示当前节点中线程已经被取消调度,当timeout或者interrupt(假如会响应中断的话)会触发节点变更为此状态,此节点的状态再不会发生变化*/
static final int CANCELLED = 1;
/**表示当前节点中线程释放资源后需要唤醒后继节点线程,在采用尾插法将新结点加入到同步队列的时候,会将新结点的前继节点设置为SIGNAL */
static final int SIGNAL = -1;
/**表示当前节点中的线程在等待一个Condition唤醒,在其他线程中调用了这个Condition的signal()会将此Node从等待队列的队头转移到同步队列的队尾,尝试竞争共享资源 */
static final int CONDITION = -2;
/**共享模式下,当前节点中的线程不仅需要唤醒后继节点,还需要唤醒后继节点的后继节点*/
static final int PROPAGATE = -3;
除了上面这四种还有一个0,表示节点初始状态,可以看出waitStatus 0) { do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; }else { /*前驱结点状态正常,将前驱结点状态设置为SIGNAL,则前驱结点释放资源的时候,就可以尝试唤醒当前这个后继节点了*/ compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; } 这个方法是在自旋中用来判断是否可以将线程安全地park(),阻塞自旋的,那么何时才能将当前线程安全地挂起呢?回想下,我们之前提到的节点的五种状态中有一种SIGNAL,表示当前节点的线程释放资源,可以唤醒后继节点,所以我们就在线程挂起之前找到它的有效的前继结点,将它的waitStatus状态设置为SIGNAL,就可以保证前继节点释放资源之后,当前节点中的线程就可以被及时地唤醒,结束阻塞了,当前线程挂起之前的准备工作都做完了,那么接下来就需要调用parkAndCheckInterrupt()方法,进行线程的挂起了。
parkAndCheckInterrupt()方法:
public final void acquire(int arg) {
/*尝试获取共享资源,尝试成功直接返回*/
if (!tryAcquire(arg)
/* 1、抢占共享资源失败,则将当前节点放入到同步队列的尾部,并标记为独占模式;
* 2、使线程阻塞在同步队列中获取资源,直到获取成功才返回;如果整个过程中被中断过就返回true,否则就返回false;*/
&& acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
/*阻塞获取资源的过程中是不响应线程中断的,内部进行了中断检测,检测到了中断请求,所以这块进行线程中断*/
selfInterrupt();
}
} 执行到这里了,说明线程可以阻塞,调用park()方法阻塞线程,等待其他线程中unpark()或者interrupt()唤醒次线程,唤醒之后执行Thread.interrupted()方法,检测阻塞过程中的线程请求中断,进入下次自旋,尝试获取共享资源。如果在阻塞获取资源的过程中,发生了异常,failed = true,则执行finally中cancelAcquire(Node)方法,取消当前节点中线程的调度。
上面说了分析了这么多,只说了线程间的获取资源时候的同步问题,那么线程间的协作在哪里体现呢?答案就是在acquireQueued(Node,int)方法中的finally块中,线程在阻塞获取共享资源的时候发生了异常,就会执行此方法将此节点从同步队列中出队,下面我们来分析下cancelAcquire(Node)方法:
cancelAcquire(Node)方法:
1 private void cancelAcquire(Node node) { 2 //当前节点为空,则说明当前线程永远不会被调度到了,所以直接返回 3 if (node == null) { 4 return; 5 } 6 7 /** 8 * 接下来将点前Node节点从同步队列出队,主要做以下几件事: 9 * 1、将当前节点不与任何线程绑定,设置当前节点为Node.CANCELLED状态;10 * 2、将当前取消节点的前置非取消节点和后置非取消节点"链接"起来;11 * 3、如果前置节点释放了锁,那么当前取消节点承担起后续节点的唤醒职责。12 */13 14 //1、取消当前节点与线程的绑定15 node.thread = null;16 17 //2、找到当前节点的有效前继节点pred18 Node pred = node.prev;19 while (pred.waitStatus > 0) {20 //为什么双向链表从后往前遍历呢?而不是从前往后遍历呢?21 node.prev = pred = pred.prev;22 }23 //用作CAS操作时候的条件判断需要使用的值24 Node predNext = pred.next;25 26 //3、将当前节点设置为取消状态27 node.waitStatus = Node.CANCELLED;28 29 /**30 * 接下来就需要将当前取消节点的前后两个有效节点"链接"起来了,"达成让当前node节点出队的目的"。31 * 这里按照node节点在同步队列中的不同位置分了三种情况:32 * 1、node节点是同步队列的尾节点tail;33 * 2、node节点既不是同步队列头结点head的后继节点,也不是尾节点tail;34 * 3、node节点是同步队列头结点head的后继节点;35 */36 37 //1、node是尾节点,并且执行过程中没有并发,直接将pred设置为同步队列的tail38 if (node == tail && compareAndSetTail(node, pred)) {39 /*40 * 此时pred已经设置为同步队列的tail,需要通过CAS操作,将pred的next指向null,没有节点再引用node,就完成了node节点的出队42 */43 compareAndSetNext(pred, predNext, null);44 }else {45 /*46 * 2、node不是尾节点,也不是头结点head的后继节点,那么当前节点node出队以后,node的有效前继结点pred,47 *就有义务在它自身释放资源的时候,唤醒node的有效后继节点successor,即将pred的状态设置为Node.SIGNAL;48 */49 int ws;50 //能执行到这里,说明当前node节点不是head的后继节点,也不是同步队列tail节点51 if (pred != head &&52 ((ws = pred.waitStatus) == Node.SIGNAL ||53 //前继节点状态虽然有效但不是SIGNAL,采用CAS操作设置为SIGNAL确保后继有效节点可以被唤醒54 (ws0) {21 s = null;22 //从同步队列的尾部向前遍历,找到当前node节点(头结点)的最近的有效后继节点23 for (Node t = tail; t != null && t != node; t = t.prev)24 if (t.waitStatus
页:
[1]