AbstractQueuedSynchronizer简称AQS,它是线程之间合作的一种标准,AQS思维来源于CLH队列锁。
CLH队列锁
在多个线程独特操作一个共享资源的时候,会将每个线程打包成一个个的QNode节点,这个节点有俩属性,一个是locked,示意以后线程是否须要获取锁,true为须要获取锁,false须要开释锁。另一个属性myPred,它是一个指针,指向本人的前驱节点QNode,每当有线程没拿到锁,将会在队列尾部排队,并将本人的myPred指向原队列尾部。每个QNode外部都会一直检测前驱节点是否须要开释锁,这样前驱节点开释了锁,本人就变成了头节点。
AbstractQueuedSynchronizer
AbstractQueuedSynchronizer抽象类的几个重要属性
// 头节点,间接把它当做 以后持有锁的线程 可能是最好了解的private transient volatile Node head;// 阻塞的尾节点,每个新的节点进来,都插入到最初,也就造成了一个链表private transient volatile Node tail;// 这个是最重要的,代表以后锁的状态,0代表没有被占用,大于 0 代表有线程持有以后锁// 这个值能够大于 1,是因为锁能够重入,每次重入都加上 1private volatile int state;// 代表以后持有独占锁的线程,举个最重要的应用例子,因为锁能够重入// reentrantLock.lock()能够嵌套调用屡次,所以每次用这个来判断以后线程是否曾经领有了锁// if (currentThread == getExclusiveOwnerThread()) {state++}private transient Thread exclusiveOwnerThread;
下图是AQS组成的 阻塞队列(绿色局部),Head和Tail只是指向阻塞队列头和尾的两个援用。
【形象队列同步器】每当线程进入阻塞状态的时候,都会打包成一个Node节点,Node节点重要属性介绍:
static final class Node { /** 示意是一个共享锁 */ static final Node SHARED = new Node(); /** 示意是一个独占锁 */ static final Node EXCLUSIVE = null; /** 这四个常量是给上面的 waitStatus 用的 */ static final int CANCELLED = 1;// 示意当火线节点消了争锁 static final int SIGNAL = -1;// 示意当火线节点须要唤醒后继节点 static final int CONDITION = -2;// 示意以后节点筹备入Condition队列 static final int PROPAGATE = -3;// 示意共享模式的流传机制 volatile int waitStatus;// 大于0示意以后节点勾销了争锁 /** 以后节点的前驱节点 */ volatile Node prev; /** 以后节点的后继节点 */ volatile Node next; /** 以后线程 */ volatile Thread thread; /** 以后节点在Condition队列中的下一个期待中的节点 */ Node nextWaiter;}
ReentrantLock
ReentrantLock是基于AQS实现的
ReentrantLock的根本应用
/** * 工作线程 * @author zhangjianbing */public class Worker { // 申明一个lock锁 private Lock lock = new ReentrantLock(); public void increament01() { // 加锁 lock.lock(); try { // TODO 业务逻辑操作 } finally { // 开释锁 lock.unlock(); } }}
ReentrantLock 在外部用了外部类 Sync 来治理锁,所以真正的获取锁和开释锁是由 Sync 的实现类来管制的,这里应用了模板办法
设计模式,比方AQS中的acquire()
办法。
abstract static class Sync extends AbstractQueuedSynchronizer { }
Sync 有两子类,别离为 NonfairSync(非偏心锁,默认)和 FairSync(偏心锁),上面是 FairSync 的源码局部。
lock()办法
个别应用lock办法进行加锁,它外部其实是调用的同步器的acquire办法。
acquire加锁
public final void acquire(int arg) { // 此时 arg == 1 ,获取锁是将 state 由0改为1 // 首先调用tryAcquire(1)试一下,如果胜利加锁,那这个线程不用打包成Node进行排队了。 if (!tryAcquire(arg) && // tryAcquire(arg)没有胜利,这个时候须要把以后线程挂起,放到阻塞队列中。 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) { selfInterrupt(); }}
tryAcquire尝试获取锁
// 尝试间接获取锁,返回值是boolean,代表是否获取到锁protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState();// volatile润饰 if (c == 0) {// 此时此刻没有线程持有锁 // 尽管此时此刻锁是能够用的,然而这是偏心锁,先判断队列中有没有其余节点 if (!hasQueuedPredecessors() && // 如果没有线程在期待,那就用CAS尝试一下,胜利了就获取到锁了, // 不胜利的话,只能阐明一个问题,就在刚刚简直同一时刻有个线程领先了 // 这里没用for循环,是因为,这只是尝试获取一下,前面会有for循环CAS compareAndSetState(0, acquires)) { // 到这里就是获取到锁了,标记一下,通知大家,当初是我占用了锁 setExclusiveOwnerThread(current); return true; } } // 如果进入这个else if分支,阐明是重入了,须要操作:state=state+1 // 这里不存在并发问题,因为以后线程曾经持有锁了 else if (current == getExclusiveOwnerThread()) {// 首先判断一下以后线程与持有锁的线程是否为同一个 int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } // 如果到这里,阐明后面的if和else if都不成立,返回false,阐明尝试失败 // 回到下面一个外层调用办法持续看: // if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // selfInterrupt(); // 持续看addWaiter入队操作 return false;}
addWaiter入队
// 此办法的作用是把线程包装成Node节点,同时进入到队列中// 参数mode此时是Node.EXCLUSIVE,代表独占模式private Node addWaiter(Node mode) { // 将以后线程包装成Node节点 Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure // 这句英文意思是enq办法下面这段代码的意义是疾速的尝试一下将以后节点设置成尾节点,如果不胜利在for循环CAS // 以下几行代码想把以后node加到链表的最初面去,也就是进到阻塞队列的最初 Node pred = tail;// 取到原始尾几点 // tail!=null => 队列不为空(tail==head的时候,其实队列是空的,不过不论这个吧) if (pred != null) { // 将以后的队尾节点,设置为以后线程的前驱节点 node.prev = pred; // 用CAS把本人设置为队尾, 如果胜利后,tail == node 了,这个节点成为阻塞队列新的尾巴 if (compareAndSetTail(pred, node)) {// 这里没用for循环CAS,目标是为了疾速尝试,不胜利则走enq // 进到这里阐明设置胜利,以后node==tail, 将本人与之前的队尾相连, // 下面曾经有 node.prev = pred,加上上面这句,也就实现了和之前的尾节点双向连贯了 pred.next = node; // 线程入队了,能够返回了 return node; } } // 如果会到这里阐明 pred==null(队列是空的) 或者 CAS失败(有线程在竞争入队) enq(node);// 代码在上面 return node;}// 采纳有限循环的形式入队,总有一天能成为队列的尾巴private Node enq(final Node node) { for (;;) { Node t = tail; // t == null阐明队列还未初始化,首先初始化队列 if (t == null) { // Must initialize // CAS设置头节点 if (compareAndSetHead(new Node())) // 这个时候有了head,然而tail指向head // 这里只是设置了tail = head,设置完了当前,持续for循环,下次就到上面的else分支了 tail = head; } else { // 这个套在有限循环里,就是将以后线程排到队尾,有线程竞争的话排不上反复排,直到排到队尾为止 node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } }}
acquireQueued获取队列
// 回到开始这段代码// if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // selfInterrupt();// 通过addWaiter(Node.EXCLUSIVE),此时曾经进入阻塞队列// 如果acquireQueued(addWaiter(Node.EXCLUSIVE), arg))返回true的话,// 意味着下面这段代码将进入selfInterrupt(),所以失常状况下,上面应该返回false// 这个办法十分重要,应该说真正的线程挂起,而后被唤醒后去获取锁,都在这个办法里了final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; // 入队后紧接着进入有限循环 for (;;) { // 拿到以后节点的前驱节点,如果前驱节点为null那么抛NPE异样 final Node p = node.predecessor(); // p == head 阐明以后节点尽管进到了阻塞队列,然而是阻塞队列的第一个,因为它的前驱是head // 阻塞队列不蕴含head节点,head个别指的是占有锁的线程,head前面的才称为阻塞队列 // 所以以后节点能够去试抢一下锁 // 这里咱们说一下,为什么能够去试试: // 首先,它是队头,这个是第一个条件,其次,以后的head有可能是刚刚初始化的node, // enq(node) 办法外面有提到,head是延时初始化的,而且new Node()的时候没有设置任何线程 // 也就是说,以后的head不属于任何一个线程,所以作为队头,能够去试一试, // tryAcquire曾经剖析过了,就是简略用CAS试操作一下state if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return interrupted; } // 到这里,阐明下面的if分支没有胜利,要么以后node原本就不是队头 // 要么就是tryAcquire(arg)没有抢赢他人 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { // 什么时候 failed 会为 true??? // tryAcquire() 办法抛异样的状况 if (failed) cancelAcquire(node); }}
shouldParkAfterFailedAcquire
// 刚刚说过,到这里就是没有抢到锁呗,这个办法说的是:"以后线程没有抢到锁,是否须要挂起以后线程?"// 第一个参数是前驱节点,第二个参数才是代表以后线程的节点private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; // 前驱节点的 waitStatus == -1 ,阐明前驱节点状态失常,以后线程须要挂起,间接能够返回true if (ws == Node.SIGNAL) return true; // 前驱节点 waitStatus大于0 ,之前说过,大于0 阐明前驱节点勾销了排队。 // 这里须要晓得这点:进入阻塞队列排队的线程会被挂起,而唤醒的操作是由前驱节点实现的。 // 所以上面这块代码说的是将以后节点的prev指向waitStatus<=0的节点, // 此while循环作用: // 如果以后节点的前驱节点勾销了排队,就找后面最近的一个状态<=0的节点,因为以后节点依赖它来唤醒 // 如果后面的节点状态都为勾销排队,那么以后节点就是队列的头 if (ws > 0) { do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { // 如果进入到这个分支意味着ws只能是0,-2,-3 // 后面的源码中,都没有看到有设置waitStatus的,所以每个新的node入队时,waitStatu都是0 // 失常状况下,前驱节点是之前的 tail,那么它的 waitStatus 应该是 0 // 用CAS将前驱节点的waitStatus设置为Node.SIGNAL(也就是-1) compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } // 这个办法返回 false,那么会再走一次 for 循序, // 而后再次进来此办法,此时会从第一个分支返回 true return false;}
这个办法完结依据返回值简略剖析下:
- 如果返回true:阐明前驱节点的waitStatus = -1,是失常状况,那么以后线程须要被挂起,期待当前被唤醒咱们也说过,当前是被前驱节点唤醒,就等着前驱节点拿到锁,而后开释锁的时候叫你好了。
- 如果返回false:阐明以后不须要被挂起。
parkAndCheckInterrupt
shouldParkAfterFailedAcquire
返回true,则进入parkAndCheckInterrupt
这个办法。这个办法的作用就是将以后线程挂起。
// 因为shouldParkAfterFailedAcquire返回true,所以须要挂起以后线程// 这里用了LockSupport.park(this)来挂起线程,期待被前驱节点唤醒private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted();}
回到下面的阻塞代码块
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true;
到这里,以后线程就被阻塞住了,它会始终在这里阻塞,直到它的前驱节点将它唤醒。
unlock()办法
release开释锁
public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false;}
tryRelease尝试开释锁
// 回到ReentrantLock看tryRelease办法// 开释锁没有应用任何CAS,那是因为自身就在锁中操作,不存在线程平安问题protected final boolean tryRelease(int releases) { // 扣减以后线程持有锁的个数(可重入锁的实现机制) int c = getState() - releases; // 判断以后线程是否是持有锁的线程 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); // 是否齐全开释锁 boolean free = false; // 其实就是重入的问题,如果c==0,也就是说没有嵌套锁了,能够开释了,否则还不能开释掉 if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free;}
unparkSuccessor唤醒后继者
// 从下面调用处晓得,参数node是head头节点private void unparkSuccessor(Node node) { int ws = node.waitStatus; // 如果head节点以后 waitStatus < 0, 将其批改为0 if (ws < 0) compareAndSetWaitStatus(node, ws, 0); // 上面的代码就是唤醒后继节点,然而有可能后继节点勾销了期待(waitStatus == 1) Node s = node.next; // 从队尾往前找,找到 waitStatus <= 0的所有节点中排在最后面的 if (s == null || s.waitStatus > 0) { s = null; // 从后往前找ws为-1的节点,如果有-1的,for循环并没有break,所以这段代码意思是找到最后面的那个节点 for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) // 唤醒后继节点的线程 LockSupport.unpark(s.thread);}
这里唤醒当前,代码又会走到这里
private final boolean parkAndCheckInterrupt() { LockSupport.park(this); // 刚刚线程被挂起在这里了 return Thread.interrupted();// 不忘检查一下以后线程的状态,true:以后线程未被中断}
到此全文完,后续会补一张偏心锁的流程图。
以上大部分来源于javadoop ,感激帮忙理清思路。