锁是面向使用者的,定义了用户调用的接口,隐藏了实现细节;
AQS是锁的实现者,屏蔽了同步状态管理,线程的排队,等待唤醒的底层操作。
锁是面向使用者,AQS是锁的具体实现者
背后复杂的线程排队,线程阻塞/唤醒,如何保证线程安全,都由AQS为我们完成了
继承AbstractQueuedSynchronizer并重写指定的方法
独占式如ReentrantLock,共享式如Semaphore,CountDownLatch
1.要实现一个独占锁,那就去重写或封装tryAcquire,tryRelease方法,
Acquire:tryAcquire(arg)、addWaiter【入队】、acquireQueued【循环获取锁,失败则挂起shouldParkAfterFailedAcquire】
Release:tryRelease(arg)、检查waitStatus、unparkSuccessor(h)【;//唤醒后继结点】
2.要实现共享锁,就去重写tryAcquireShared,tryReleaseShared
acquireShared:tryAcquireShared、doAcquireShared(arg)【setHeadAndPropagate】;【尝试获取(state+1),入队、等待唤醒】
realseShared:tryRealseShared、doReleaseShared()【CAS下的 unparkSuccessor(h)】;【state-1并判断、唤醒后继】
CANCELLED 1:因为超时或者中断,结点被设置为取消状态,不能去竞争锁,不能转换为其他状态;会被检测并踢出队列,被GC回收;
SIGNAL -1:表示这个结点的继任结点被阻塞,到时需要唤醒它
CONDITION -2:表示这个结点在条件队列中,因为等待某个条件而被阻塞;
共享传播状态
PROPAGATE -3:使用在共享模式头结点有可能牌处于这种状态,表示锁的下一次获取可以无条件传播;
N个线程调用await阻塞在for循环里面,然后N个线程依次调用countDown,每调用一次state减1,直接state为0,这些线程退出for循环(解除阻塞)!
退出for循环时,由于头结点状态标志位为PROPAGATE,而且这些结点都是共享模式,由头结点一传播,这些结点都获取锁,于是齐头并进执行了......
countDown()方法调用 releaseShared(int arg),直到state-1后为0.开始执行传递唤醒后继shared线程
void countDown{
if (tryReleaseShared(arg)) { // state-1后是否为 0
doReleaseShared(); //检测共享节点和等待状态为 PROPAGATE -3的后继节点,开始唤醒unpark(thread);
return true;
}
}
await方法使当前线程一直等待,阻塞在doAcquireShared方法中,除非线程被中断,或者 state == 0;
当调用countDown方法之后state-1,当锁存器减少到0时,await方法就会返回
两次doReleaseShared();的区别
Acquire:tryAcquire(arg)、addWaiter【线程入队】、acquireQueued【循环获取锁,失败则挂起shouldParkAfterFailedAcquire,等待头结点唤醒】
Release:tryRelease(arg)【每次state-1】、unparkSuccessor(h)【若state==0,唤醒后继结点】
获取acquire(int arg)
a.首先,调用使用者重写的tryAcquire方法,若返回true,意味着获取同步状态成功,后面的逻辑不再执行;若返回false,也就是获取同步状态失败,进入b步骤;
b.此时,获取同步状态失败,构造独占式同步结点,通过addWatiter将此结点添加到同步队列的尾部(此时可能会有多个线程结点试图加入同步队列尾部,需要以线程安全的方式添加);
c.该结点以在队列中尝试获取同步状态,若获取不到,则阻塞结点线程,直到被前驱结点唤醒或者被中断
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt();//请求锁成功,加入等待队列,中断自己,等待被唤醒 }
tryAcquire(arg)会调用nonfairTryAcquire(1),含插队
获取当前线程
获取并检查锁状态,若c==0,CAS将state置为1,并设置当前线程获取独占锁,返回true
else,查看当前线程是不是已经是独占锁。若是,state+1,返回true
都不是,返回false
final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread();//获取当前线程 int c = getState();//获取锁数量 if (c == 0) {//如果锁数量为0,证明该独占锁已被释放,当下没有线程在使用 if (compareAndSetState(0, acquires)) {//继续通过CAS将state由0变为1,注意这里传入的acquires为1; setExclusiveOwnerThread(current);//将当前线程设置为独占锁的线程 return true; } } else if (current == getExclusiveOwnerThread()) {//查看当前线程是不是就是独占锁的线程 int nextc = c + acquires;//如果是,锁状态的数量为当前的锁数量+1 if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc);//设置当前的锁数量 return true; } return false; }
若请求锁失败,addWaiter 将当前线程链入队尾并挂起,之后等待被唤醒 【快速、正常】
acquireQueued 入队成功后,返回node节点,继续第三次插队
无限循环调用:获取node的前驱节点p
p==head&&tryAcquire(1) 是唯一跳出循环的方法:p成为头结点并且获取锁成功:如果p是头节点,就继续使用tryAcquire(1)方法插队,若成功,不用中断,第三次插队成功;【只有前驱结点是头结点的结点,也就是老二结点,才有机会去tryAcquire;】
挂起后后 跳出循环,需要中断自身
LockSupport.park(this);return Thread.interrupted(); //挂起当前的线程,后等待前驱节点unpark唤醒该线程;唤醒方法为public
释放 release(int arg)
//释放锁的操作
public final boolean release(int arg)
//尝试释放锁,若释放后state==0,成功,唤醒后继节点;否则失败直接返回false;若成功,判断waitStatus不等于0,唤醒后继节点 if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
调用tryRelease释放锁,如果释放失败,直接返回
protected final boolean tryRelease(int releases) {
//获取state值,释放一定值 int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false;
//如果差是0,表示锁已经完全释放 if (c == 0) { free = true;
//下面设置为null表示当前没有线程占用锁 setExclusiveOwnerThread(null); }
//如果c不是0表示锁还没有完全释放,修改state值 setState(c); return free; }
释放锁成功后需要唤醒继任结点,是通过方法unparkSuccessor实现
若后继结点为空或处于CANCEL状态,从后向前遍历找寻一个正常的结点,唤醒其对应线程
private void unparkSuccessor(Node node) { //传进来头结点
int ws = node.waitStatus;
if (ws < 0)//检查头结点的waitStatus位,小于0表示没被取消
compareAndSetWaitStatus(node, ws, 0);//将当前节点的状态修改为0
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
//从尾向头寻找,tail开始,寻找pre
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
//查看头结点的下一个结点,如果下一个结点不为空,且waitStatus<=0,表示后继结点没有被取消
if (s != null)
LockSupport.unpark(s.thread);
}
因为在CLH队列中的结点随时有可能被中断,被中断的结点的waitStatus设置为CANCEL,而且它会被踢出CLH队列,如何个踢出法,就是它的前趋结点的next并不会指向它,而是指向下一个非CANCEL的结点,而它自己的next指针指向它自己。一旦这种情况发生,如何从头向尾方向寻找继任结点会出现问题,因为一个CANCEL结点的next为自己,那么就找不到正确的继任接点
CANCEL结点的next指针为什么要指向它自己,为什么不指向真正的next结点?为什么不为NULL?
第一个问题的答案是这种被CANCEL的结点最终会被GC回收,如果指向next结点,GC无法回收
第二个问题的回答,为了使isOnSyncQueue方法更新简单,判断一个结点是否在同步队列中,指向它自己时表示在,可以踢出
如果一个结点next不为空,那么它在同步队列中,如果CANCEL结点的后继为空那么CANCEL结点不在同步队列中,这与事实相矛盾。
因此将CANCEL结点的后继指向它自己是合理的选择。
acquireShared:tryAcquireShared、doAcquireShared(arg)【setHeadAndPropagate】;
realseShared:tryRealseShared、doReleaseShared()【unparkSuccessor(h)】;
lock,调用acquireShared
public void lock() {
sync.acquireShared(1);
}
//获取共享锁API acquireShared
public final void acquireShared(int arg) {
//state != 0时,tryAcquireShared(arg) < 0,才会真正操作锁;表示获取锁失败
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
tryAcquireShared(arg) 判断是否需要唤醒后续节点获取共享锁
tryAcquireShared(arg):return (getState() == 0) ? 1 : -1;返回state是否为0
protected int tryAcquireShared(int acquires) { return (getState() == 0) ? 1 : -1; }
< 0:表示state != 0,获取锁失败,执行doAcquireShared(arg)
//= 0:表示当前线程获取共享锁成功,但不需要把它后面等待的节点唤醒
> 0:state==0,表示当前线程获取共享锁成功,且此时需要把后续节点唤醒让它们去尝试获取共享锁
获取锁失败后 操作doAcquireShared(int arg) ,里面继续循环尝试获取共享锁,若成功,可以传播状态;失败则入队挂起
大体逻辑与独占式的acquireQueued差距不大,只不过由于是共享式,会有多个线程同时获取到线程,也可能同时释放线程,空出很多同步状态,
所以当排队中的老二获取到同步状态,如果还有可用资源,会继续传播下去。
释放共享锁ReleaseShared(int arg)
tryReleaseShared(arg):释放共享锁,state-1
state-1后,返回是否为0;相当于一个栅栏,只有当state==0,继续执行doReleaseShared()
if (tryReleaseShared(arg)) {//state为0时,返回true(针对CountDownLatch)
doReleaseShared();
return true;
}
doReleaseShared(); 调用unparkSuccessor(h);传递唤醒阻塞的共享线程
死循环,共享模式,持有同步状态的线程可能有多个,采用循环CAS保证线程安全
释放同步状态也是多线程的,此处采用了CAS自旋来保证
private void doReleaseShared() { for (;;) {//死循环,共享模式,持有同步状态的线程可能有多个,采用循环CAS保证线程安全 Node h = head; if (h != null && h != tail) { int ws = h.waitStatus; if (ws == Node.SIGNAL) { if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; unparkSuccessor(h);//唤醒后继结点 } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; } if (h == head) break; } }
1)共享锁初始化时会给state设值,所有请求锁的共享节点都会放入SyncQueue中阻塞
2)一个节点A获取锁(成为head节点)之后,会唤醒它的下一个共享节点线程B,B唤醒后会去竞争锁,一直往下,直到后面的共享节点都唤醒为止。
此时所有共享节点都获取了锁,都可以往下执行了
如果state值代表的许可数足够使用,那么请求线程将会获得同步状态即对共享资源的访问权,并更新state的值(一般是对state值减1),但如果state值代表的许可数已为0,则请求线程将无法获取同步状态,线程将被加入到同步队列并阻塞,直到其他线程释放同步状态(一般是对state值加1)才可能获取对共享资源的访问权
acquireShared()方法获取锁
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);//构造一个共享结点,添加到同步队列尾部。若队列初始为空,先添加一个无意义的dummy结点,再将新节点添加到队列尾部
boolean failed = true;//是否获取成功
try {
boolean interrupted = false;//线程parking过程中是否被中断过
for (;;) {//自旋
final Node p = node.predecessor();//获取前驱结点
if (p == head) {//头结点持有同步状态,只有前驱是头结点,才有机会尝试获取同步状态
int r = tryAcquireShared(arg);//尝试获取同步资源
if (r >= 0) {//r>=0,获取成功
setHeadAndPropagate(node, r);//获取成功就将当前结点设置为头结点,若还有可用资源,传播下去,也就是继续唤醒后继结点,即doReleaseShared();
p.next = null; // 方便GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&//是否能安心进入parking状态
parkAndCheckInterrupt())//阻塞线程
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
//setHeadAndPropagate 把node节点设置成head节点,且node.waitStatus->Node.PROPAGATE*
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;//h用来保存旧的head节点
setHead(node);//head引用指向node节点
/* 这里意思有两种情况是需要执行唤醒操作
* 1.propagate > 0 表示调用方指明了后继节点需要被唤醒
* 2.头节点后面的节点需要被唤醒(waitStatus<0),不论是老的头结点还是新的头结点*/
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())//node是最后一个节点或者 node的后继节点是共享节点
/* 如果head节点状态为SIGNAL,唤醒head节点线程,重置head.waitStatus->0
* head节点状态为0(第一次添加时是0),设置head.waitStatus->Node.PROPAGATE表示状态需要向后继节点传播
*/
doReleaseShared();//对于这个方法,其实就是把node节点设置成Node.PROPAGATE状态
}
}
tryAcquire 函数是尝试获取写锁:1.如果有读线程或者写线程且不是当前线程,直接失败;2.如果写锁的count超过了65535,直接失败