第6章 Java并发包中锁原理剖析
1. LockSupport工具类
JDK 中的jr.jar包里面的LockSupport是个工具类,主要作用是挂起和唤醒线程.
LockSupport类与每个使用它的线程都会关联一个许可证(锁),在默认情况下调用时是不具有许可证的.
(1). void park()方法
如果没有许可证,挂起.
(2). void unpark(Thread thread)方法
thread线程立即获取许可证,如果当前状态为被阻塞,立即唤醒.
(3). void parkNanos(long nanos)方法
如果没有许可证,挂起nanos微秒
(4). park(Object blocker)方法
将blocker变量存放到调用park方法挂起的线程中,推荐将this放入,可以从日志中知道在那个类中的代码发生了挂起
(5). void parkNanos(Object blocker, long nanos)方法
相比上个方法多了超时时间
(6). void parkUntil(Object blocker, long deadline)方法
阻塞到时间戳deadline.
2. AQS-----锁的底层支持
AbstractQueuedSynchronizer抽象同步队列简称AQS,是实现同步器的基础组件,并发包中锁的底层就是使用AQS实现的.
AQS是一个FIFO的双向队列,队列元素为Node. Node中的thread用来存放进入AQS队列的线程.Node节点内部waitStatus记录当前线程等待状态:CANCELLED(1,线程被取消),SIGNAL(-1,线程需要被唤醒),CONDITION(-2,线程在条件队列中等待),PROPAGATE(-3,释放共享资源时需要通知其他节点).
AQS中维持了一个单一的状态信息state,可以通过CAS操作修改其值,并且它有get()和set()方法.
对于AQS来说,线程同步的关键对状态值state进行操作,分为独占方式(标记,如果标记状态不正确不能进行操作)和共享方式(直接进行CAS修改,不需要判断标记).
(1). 独占不响应中断模式下,获取与释放资源
AbstractQueuedSynchronizer源码剖析(二)- 不响应中断的独占锁
1). 获取锁的过程
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
private Node addWaiter(Node mode) {
//基于当前线程,节点类型(Node.EXCLUSIVE)创建新的节点
//由于这里是独占模式,因此节点类型就是Node.EXCLUSIVE
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
//这里为了提搞性能,首先执行一次快速入队操作,即直接尝试将新节点加入队尾
if (pred != null) {
node.prev = pred;
//这里根据CAS的逻辑,即使并发操作也只能有一个线程成功并返回,其余的都要执行后面的入队操作。即enq()方法
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) {
//同样是CAS,只有一个线程可以初始化头结点成功,其余的都要重复执行循环体
if (compareAndSetHead(new Node()))
tail = head;
} else {
//新创建的节点指向队列尾节点,毫无疑问并发情况下这里会有多个新创建的节点指向队列尾节点
node.prev = t;
//基于这一步的CAS,不管前一步有多少新节点都指向了尾节点,这一步只有一个能真正入队成功,其他的都必须重新执行循环体
if (compareAndSetTail(t, node)) {
t.next = node;
//该循环体唯一退出的操作,就是入队成功(否则就要无限重试)
return t;
}
}
}
}
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)) {
//当前节点获得锁资源以后设置为头节点,这里继续理解我上面说的那句话
//头结点就表示当前正占有锁资源的节点
setHead(node);
p.next = null; //帮助GC
//表示锁资源成功获取,因此把failed置为false
failed = false;
//返回中断标记,表示当前节点是被正常唤醒还是被中断唤醒
return interrupted;
}
//如果没有获取锁成功,则进入挂起逻辑
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
//最后会分析获取锁失败处理逻辑
if (failed)
cancelAcquire(node);
}
}
//首先说明一下参数,node是当前线程的节点,pred是它的前置节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取前置节点的waitStatus
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
//如果前置节点的waitStatus是Node.SIGNAL则返回true,然后会执行parkAndCheckInterrupt()方法进行挂起
return true;
if (ws > 0) {
//由waitStatus的几个取值可以判断这里表示前置节点被取消
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
//这里我们由当前节点的前置节点开始,一直向前找最近的一个没有被取消的节点
//注,由于头结点head是通过new Node()创建,它的waitStatus为0,因此这里不会出现空指针问题,也就是说最多就是找到头节点上面的循环就退出了
pred.next = noparkAndCheckInterrupt()de;
} else {
//根据waitStatus的取值限定,这里waitStatus的值只能是0或者PROPAGATE,那么我们把前置节点的waitStatus设为Node.SIGNAL然后重新进入该方法进行判断
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
- 当前线程调用acquire()申请获取锁资源
- 先调用tryAcquire()尝试获取锁()
- 这个方法内部由具体的锁实现
- 如果失败先调用addWaiter()将当前线程入队到AQS队列中
- 如果之前没有创建队列(队尾为null),直接调用enq进行完整的入队操作(包括初始化)
- 是一个自旋循环,第一次循环创建一个空结点,设置为队尾和队首,第二次循环时将传入的node入队
- 如果已经有队尾存在了,先将node(待插入结点)的前置设置为tail,使用t指针指向tail,使用CAS将tail指针指向node,然后将实际上的队尾t的next设置为node.
- 如果之前没有创建队列(队尾为null),直接调用enq进行完整的入队操作(包括初始化)
- 然后在acquireQueued()方法中等待被唤醒直到获取到资源.
- 进入acquireQueued方法后会进入一个无限循环.每一次循环都会判断传入的参数是不是队首,并且尝试一次锁获取
- 如果获取成功,将自己设置为队首,并将node.thread设置为null,保证头结点永远是一个不带thread的空节点.
- 如果获取失败,调用shouldParkAfterFailedAcquire()判断自己需不需要阻塞.阻塞的大前提是前继节点是可被唤醒的(waitStatus设置为Signal),这样才能让自己有机会被唤醒(队列中按顺序唤醒)
- 判断传入的前继节点的waitStatus是否为Signal,是的话直接返回ture
- 根据waitStatus的值分为两种情况
- 如果waitStatus大于0:也就是前继节点被取消了.一直向前查找一个没有被取消的节点(waitStatus>=0)的结点(头结点waitStatus为0或-1,因此不可能出现空指针),并拼接成双向队列(后面的队列都是waitStatus>=0的,在状态表中只有被取消的线程是这个状态,所以被移除出队列,下一次GC会被清除),返回false
- 如果waitStatus不大于0,也就是为0,-2或-3:修改前继节点waitStatus为-1
- 如果shouldParkAfterFailedAcquire()返回true,证明node的前继节点可以被唤醒,立即调用parkAndCheckInterrupt()阻塞挂起node
- 如果shouldParkAfterFailedAcquire()返回false,证明在方法内部找到了一个可以被唤醒的节点作为前继节点.在下一次循环中进行挂起
- parkAndCheckInterrupt()方法的挂起并不会被中断打断,是非中断挂起,如果挂起过程中出现了中断,方法在返回时会返回true,否则返回false
- 如果上一步返回了true,证明出现了中断,在下一次循环中将中断标记返回到上一层代码.
- 线程被唤醒之后会将返回中断标记到这里,如果出现过中断,将自身线程挂起,来弥补之前的中断.
- 先调用tryAcquire()尝试获取锁()
- 进入临界区(线程中运行的用户代码)
2). 锁的释放过程
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
//把标记为设置为0,表示唤醒操作已经开始进行,提高并发环境下性能
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
//如果当前节点的后继节点为null,或者已经被取消
if (s == null || s.waitStatus > 0) {
s = null;
//注意这个循环没有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);
}
释放锁的逻辑并不复杂.
- 调用tryRelease()来释放资源.(判断当前线程是否和AQS中的线程一致,然后对state进行修改)(这里返回值是指是否已经完全释放资源了,可重入锁需要将state释放至0)
- 拿到head节点,判断其状态,如果不为null,且waitStatus不为0(初始状态下,为0,当这个节点有了后继节点时,会被修改成-1,这里是为了防止只有一个头节点的空队列进行了释放锁),调用unparkSuccessor()唤醒头结点的后继节点的线程
- 先进行waitStatus的判断,如果waitStatus<0,将其设置为0,避免其他线程也进入此方法,提高高并发环境下的性能.
- 找到头结点的后继节点,再次判断是否为空或者其状态值是否被取消
- 如果后继节点为空或者状态为被取消,从后向前找到最前面的一个等待唤醒的节点
- 执行唤醒操作
- 返回true
如果唤醒的节点前有被取消的节点,那么会在唤醒后被判断前继节点不是头结点,再次进行判断前继节点是否能被唤醒的方法调用.在这个方法调用中会将前面的被取消的节点移除,然后再次进行前继节点是不是头结点的判断.这次就可以判断成功,将此节点设置为头结点,成功唤醒,运行其内部用户代码.
(2). 共享不响应中断模式下,获取与释放资源
AbstractQueuedSynchronizer源码剖析(四)- 不响应中断的共享锁
1). 获取锁的过程
public final void acquireShared(int arg) {
//尝试获取共享锁,返回值小于0表示获取失败
if (tryAcquireShared(arg) < 0)
//执行获取锁失败以后的方法
doAcquireShared(arg);
}
//参数不多说,就是传给acquireShared()的参数
private void doAcquireShared(int arg) {
//添加等待节点的方法跟独占锁一样,唯一区别就是节点类型变为了共享型,不再赘述
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
//表示前面的节点已经获取到锁,自己会尝试获取锁
if (p == head) {
int r = tryAcquireShared(arg);
//注意上面说的, 等于0表示不用唤醒后继节点,大于0需要
if (r >= 0) {
//这里是重点,获取到锁以后的唤醒操作,后面详细说
setHeadAndPropagate(node, r);
p.next = null;
//如果是因为中断醒来则设置中断标记位
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
//挂起逻辑跟独占锁一样,不再赘述
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
//获取失败的取消逻辑跟独占锁一样,不再赘述
if (failed)
cancelAcquire(node);
}
}
//两个入参,一个是当前成功获取共享锁的节点,一个就是tryAcquireShared方法的返回值,注意上面说的,它可能大于0也可能等于0
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; //记录当前头节点
//设置新的头节点,即把当前获取到锁的节点设置为头节点
//注:这里是获取到锁之后的操作,不需要并发控制
setHead(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())
//后面详细说
doReleaseShared();
}
}
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
- 调用acquireShared()去获取锁
- 首先会尝试调用tryAcquireShared()共享方式获取资源,返回值负数表示失败,0表示成功,但没有剩余资源可用,整数表示成功且有剩余资源
- 当返回负数时,调用doAcquireShared()进行线程的入队列挂起等待
- 将当前线程打包成Node对象,类型为共享型,添加到队列尾,方法与独占式相同
- 如果当前节点的前继节点是头结点,尝试进行获取资源,如果返回值不为负,意味着可以获取到资源,调用setHeadAndPropagate()将当前节点设置为头结点,判断是否是因为唤醒线程来到这里,对中断标记进行判断,是否进行中断补充
- setHeadAndPropagate(Node node, int propagate)的第二个参数是刚刚尝试获取资源得到的剩余资源数,如果大于零,代表有多余资源,那么应该去唤醒下一个可被唤醒的线程
- h == null 场景未知,应该不会出现吧.
- h.waitStatus意味着头结点为可唤醒状态
- 以上的集中状况都需要进行线程唤醒的尝试.如果当前头结点的前继节点不为空且为共享型,调用doReleaseShared()对头结点后的第一个可唤醒节点进行唤醒.
- 如果前继节点不是头节点,挂起,逻辑同独占锁
2). 释放锁的过程
public final boolean releaseShared(int arg) {
//尝试释放共享锁
if (tryReleaseShared(arg)) {
//唤醒过程,详情见上面分析
doReleaseShared();
return true;
}
return false;
}
private void doReleaseShared() {
for (;;) {
//唤醒操作由头结点开始,注意这里的头节点已经是上面新设置的头结点了
//其实就是唤醒上面新获取到共享锁的节点的后继节点
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
//表示后继节点需要被唤醒
if (ws == Node.SIGNAL) {
//这里需要控制并发,因为入口有setHeadAndPropagate跟release两个,避免两次unpark
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
//执行唤醒操作
unparkSuccessor(h);
}
//如果后继节点暂时不需要唤醒,则把当前节点状态设置为PROPAGATE确保以后可以传递下去
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}unparkSuccessor
//如果头结点没有发生变化,表示设置完成,退出循环
//如果头结点发生变化,比如说其他线程获取到了锁,为了使自己的唤醒动作可以传递,必须进行重试
if (h == head)
break;
}
}
- 调用releaseShared()获取资源
- 先调用tryReleaseShared()尝试释放共享锁,由具体的锁实现逻辑,如果获取到了资源,返回true.继而调用doReleaseShared()唤醒头结点后的可唤醒线程
- 进入一个自旋循环
- 判断头结点(如果从设置头结点调用至此方法,此时头结点是新的头节点)是否为空,是否只有一个头结点组成空队列.
- 如果都不是,那么头结点有效,然后判断头结点状态,如果为-1,设置为0,
- 调用unparkSuccessor()执行唤醒操作(别的线程在21行会判断为true,跳过,这样就控制了并发)
- unparkSuccessor()与独占锁相同,如果头结点的后继节点不能唤醒,从后向前找到最前面的一个等待唤醒的节点唤醒它
- 然后跳入27行将状态改为共享模式下可释放,适应在设置头结点处的调用.
- 如果头结点没有变化(并发情况下,其他线程可能会改变head的引用),退出方法.
- 先调用tryReleaseShared()尝试释放共享锁,由具体的锁实现逻辑,如果获取到了资源,返回true.继而调用doReleaseShared()唤醒头结点后的可唤醒线程
3. AQS-----条件变量的支持
和之前讲到的notify()和wait()配合synchronized内置锁实现线程间同步一样,AQS中的条件变量**signal()和await()**方法也是用来配合锁(使用AQS实现)来实现线程间同步的.
不同点在于,synchronized同时之能与一个共享变量的notify()或wait()方法实现同步,二AQS的一个锁可以对应多个变量.
(1). 条件队列阻塞的过程(功能类比wait()方法)
//条件队列入口,参考上面的代码片段
public final void await() throws InterruptedException {
//如果当前线程被中断则直接抛出异常
if (Thread.interrupted())
throw new InterruptedException();
//把当前节点加入条件队列
Node node = addConditionWaiter();
//释放掉已经获取的独占锁资源
int savedState = fullyRelease(node);
int interruptMode = 0;
//如果不在同步队列中则不断挂起
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
//中断处理,另一种跳出循环的方式
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//走到这里说明节点已经条件满足被加入到了同步队列中或者中断了
//这个方法很熟悉吧?就跟独占锁调用同样的获取锁方法,从这里可以看出条件队列只能用于独占锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
//走到这里说明已经成功获取到了独占锁,接下来就做些收尾工作
//删除条件队列中被取消的节点
if (node.nextWaiter != null)
unlinkCancelledWaiters();
//根据不同模式处理中断
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
//注:1.与同步队列不同,条件队列头尾指针是firstWaiter跟lastWaiter
//注:2.条件队列是在获取锁之后,也就是临界区进行操作,因此很多地方不用考虑并发
private Node addConditionWaiter() {
Node t = lastWaiter;
//如果最后一个节点被取消,则删除队列中被取消的节点
//至于为啥是最后一个节点后面会分析
if (t != null && t.waitStatus != Node.CONDITION) {
//删除所有被取消的节点
unlinkCancelledWaiters();
t = lastWaiter;
}
//创建一个类型为CONDITION的节点并加入队列,由于在临界区,所以这里不用并发控制
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
//删除取消节点的逻辑虽然长,但比较简单,就不单独说了,就是链表删除
private void unlinkCancelledWaiters() {
Node t = firstWaiter;
Node trail = null;
while (t != null) {
Node next = t.nextWaiter;
if (t.waitStatus != Node.CONDITION) {
t.nextWaiter = null;
if (trail == null)
firstWaiter = next;
else
trail.nextWaiter = next;
if (next == null)
lastWaiter = trail;
}
else
trail = t;
t = next;
}
}
//入参就是新创建的节点,即当前节点
final int fullyRelease(Node node) {
boolean failed = true;
try {
//这里这个取值要注意,获取当前的state并释放,这从另一个角度说明必须是独占锁
//可以考虑下这个逻辑放在共享锁下面会发生什么?
int savedState = getState();
//跟独占锁释放锁资源一样,不赘述
if (release(savedState)) {
failed = false;
return savedState;
} else {
//如果这里释放失败,则抛出异常
throw new IllegalMonitorStateException();
}
} finally {
//如果释放锁失败,则把节点取消,由这里就能看出来上面添加节点的逻辑中只需要判断最后一个节点是否被取消就可以了
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
//判断节点是否在同步队列中
final boolean isOnSyncQueue(Node node) {
//快速判断1:节点状态或者节点没有前置节点
//注:同步队列是有头节点的,而条件队列没有
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
//快速判断2:next字段只有同步队列才会使用,条件队列中使用的是nextWaiter字段
if (node.next != null)
return true;
//上面如果无法判断则进入复杂判断
return findNodeFromTail(node);
}
//注意这里用的是tail,这是因为条件队列中的节点是被加入到同步队列尾部,这样查找更快
//从同步队列尾节点开始向前查找当前节点,如果找到则说明在,否则不在
private boolean findNodeFromTail(Node node) {
Node t = tail;
for (;;) {
if (t == node)
return true;
if (t == null)
return false;
t = t.prev;
}
}
//这里的判断逻辑是:
//1.如果现在不是中断的,即正常被signal唤醒则返回0
//2.如果节点由中断加入同步队列则返回THROW_IE,由signal加入同步队列则返回REINTERRUPT
private int checkInterruptWhileWaiting(Node node) {
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
//修改节点状态并加入同步队列
//该方法返回true表示节点由中断加入同步队列,返回false表示由signal加入同步队列
final boolean transferAfterCancelledWait(Node node) {
//这里设置节点状态为0,如果成功则加入同步队列
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
//与独占锁同样的加入队列逻辑,不赘述
enq(node);
return true;
}
//如果上面设置失败,说明节点已经被signal唤醒,由于signal操作会将节点加入同步队列,我们只需自旋等待即可
while (!isOnSyncQueue(node))
Thread.yield();
return false;
}
//根据中断时机选择抛出异常或者设置线程中断状态
private void reportInterruptAfterWait(int interruptMode) throws InterruptedException {
if (interruptMode == THROW_IE)
throw new InterruptedException();
else if (interruptMode == REINTERRUPT)
//实现代码为:Thread.currentThread().interrupt();
selfInterrupt();
}
- 调用await()方法将当前线程移入条件队列,等待条件队列唤醒
- 如果当前线程被中断,抛出异常
- 调用addConditionWaiter()将当前线程加入条件队列
- 如果最后一个节点被取消了,调用unlinkCancelledWaiters()清理队列中的被取消节点
- 该方法的逻辑就是从头遍历队列,如果状态为被取消,删除该节点
- 创建一个类型为条件等待的节点加入队列尾
- 返回该节点
- 如果最后一个节点被取消了,调用unlinkCancelledWaiters()清理队列中的被取消节点
- 调用fullyRelease()释放掉当前线程占用的资源,这一步之前的操作都是线程安全的
- 获取当前的state值,调用release()释放资源(内部逻辑之前有讲),如果成功则返回并标记为成功,失败抛出异常,
- 最后如果没有成功,也就是释放锁失败了,将节点取消(从这里可以看出来,被取消的节点一定是结尾处的,所以上面addConditionWaiter()方法判断的是最后一个节点是否被取消,这样保证了队列中间不可能出现被取消节点)
- 循环调用isOnSyncQueue()判断当前线程是否在同步队列中(如果不在则相当于内置锁中被wait()阻塞但没有被notify()唤醒的状态),这里是调用通过其他线程调用signal()方法将该线程(条件)唤醒
- 先进行快速判断,如果该节点状态为条件等待(肯定在条件队列中),或者前继节点(同步队列中)为空,证明不在同步队列中
- 如果后继节点(同步队列中)不为空,则证明在同步队列中,因为只有在队列中的节点才有可能被添加后继节点
- 如果上面两个都不能判断,调用findNodeFromTail()进入复杂判断
- 从尾节点向前查找节点,如果找到了当前节点,证明当前节点在同步队列中
- 如果不在同步队列中,进行挂起.并进行中断处理
- 然后在acquireQueued()方法中等待被唤醒直到获取到资源.(内部逻辑之前有描述过)
- 如果,当前node不是尾节点,说明其他线程也对条件队列进行了操作,调用unlinkCancelledWaiters()清理队列中被取消的节点
- 根据不同模式,判断是否调用reportInterruptAfterWait()处理中断
- 根据中断时机选择抛出异常或者中断补偿(取决于能否响应中断)
(2). 条件队列唤醒的过程(功能类比notify()方法)
//条件队列唤醒入口
public final void signal() {
//如果不是独占锁则抛出异常,再次说明条件队列只适用于独占锁
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//如果条件队列不为空,则进行唤醒操作
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
//该方法就是把一个有效节点从条件队列中删除并加入同步队列
//如果失败则会查找条件队列上等待的下一个节点直到队列为空
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&(first = firstWaiter) != null);
}
//将节点加入同步队列
final boolean transferForSignal(Node node) {
//修改节点状态,这里如果修改失败只有一种可能就是该节点被取消,具体看上面await过程分析
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
//该方法很熟悉了,跟独占锁入队方法一样,不赘述
Node p = enq(node);
//注:这里的p节点是当前节点的前置节点
int ws = p.waitStatus;
//如果前置节点被取消或者修改状态失败则直接唤醒当前节点
//此时当前节点已经处于同步队列中,唤醒会进行锁获取或者正确的挂起操作
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
- 调用signal()条件唤醒条件队列中的一个线程,将该线程从条件队列中移动到同步队列中
- 如果不是独占锁,抛出异常,条件队列只能用于独占锁
- 如果条件队列不为空,调用doSignal()执行唤醒操作
- 从传入结点(上一步传入的是头结点)开始把一个有效节点从条件队列删除
- 这一步使用first保存头结点,然后头结点后移(循环会判断头结点为空退出循环)
- 调用transferForSignal()将刚刚得到的first节点移入同步队列(成功则退出循环)
- 使用AQS修改该节点的状态,将CONDITION(条件等待)修改为0(如果修改失败肯定是线程被取消了或者其他线程在这一步状态为刚刚修改的0,因为条件队列中的线程状态只能是CONDITION或者被取消,这一步修改的不算)如果失败,返回false
- 调用enq()将此节点入同步队列,并返回前继节点(逻辑不再赘述)
- 如果前继节点被取消或者修改状态失败,挂起当前节点,唤醒后进入await()中的acquireQueued()部分自旋挂起等待唤醒(这个唤醒是锁的竞争,而不是条件控制的).
- 然后会回到上层方法调用,循环判断当前线程是否在同步队列中…
4. 独占锁ReentrantLock的原理
(1). 结构
ReentrantLock是可重入的独占锁,同时只能有一个线程可以获取这个锁,其他线程尝试获取就会被阻塞并放入AQS阻塞队列中.
底层是使用AQS来实现的,根据参数来决定其内部是否公平内部类sync如果是NonfairSync类型的,就非公平,如果是FairSync,则公平.
(2). 获取锁
1). void lock()方法
public void lock() {
sync.lock();
}
// 非公平锁
final void lock() {
if (compareAndSetState(0, 1))// 如果当前锁是自由的,就直接获取,这就是竞争锁
setExclusiveOwnerThread(Thread.currentThread());
else// 如果锁被占有,那么去排队
acquire(1);
}
// 公平锁
final void lock() {// 公平锁,就要排队
acquire(1);
}
// 通用
public final void acquire(int arg) {// 同之前AQS
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// 非公平锁
// 非公平性体现在如果一个线程释放了锁,在进行下一步唤醒队列中的节点之前别的线程直接竞争到了锁,那么队列中的节点会唤醒失败
protected final boolean tryAcquire(int acquires) {// 进行了一次调用,逻辑差不多
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
//先获取线程和锁的状态
final Thread current = Thread.currentThread();
int c = getState();
// 这整个if-elseif都可以让没有进行排队直接获取锁的线程直接拿到锁
if (c == 0) {// 如果锁空闲
if (compareAndSetState(0, acquires)) {// 获取到锁
setExclusiveOwnerThread(current);
return true;
}
}else if (current == getExclusiveOwnerThread()) {//如果锁不空闲,但是是自己拿到的
// 锁重入
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// 锁不空闲而且不属于自己,就只能是别人的锁,尝试获取失败
return false;
}
// 公平锁
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 不同点就在这,hasQueuedPredecessors()方法中当前线程节点有前继节点返回true,AQS队列为空或当前线程是AQS的第一个节点返回false
// 简单点说就是确保当前节点是头结点后的第一个节点,也就是排到队首的节点,保证只有队首节点才能被唤醒
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;
}
非公平锁当锁自由时,就可以直接获取锁,而公平锁需要去acquire()方法内部判断.
两种锁调用的acquire()方法都一样.
tryAcquire()方法有所区别,实现逻辑的区别在于公平锁在锁空闲的情况下,获取锁之前确保了当前节点是头结点后的第一个节点,从而让唤醒的节点一定是队列中排过队的.
2). void lockInterruptibly()方法
对中断响应的获取锁.逻辑都类似,调用的是AQS中可被中断的获取锁方法
3). boolean tryLock()方法
尝试获取锁,如果没获取到不会阻塞
public boolean tryLock() {
// 之前介绍过nonfairTryzaiAcquire(),就是单纯的获取锁,之前的阻塞的逻辑是在acquire中
return sync.nonfairTryAcquire(1);
}
4). boolean tryLock(long timeout, TimeUnit unit)方法
设置了时间,如果在该时间内没有获取到锁,返回false.
TimeUnit参数为时间粒度,直接new一个TimeUnit对象即可.
(3). 释放锁
1). void unlock()方法
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
// 逻辑同AQS
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
// c为本次重入成功后,锁的重入次数
int c = getState() - releases;
// 如果不是当前线程拿到锁,抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 如果这次重入之后,重入次数为零,证明锁没有被任何线程重入,清空锁持有线程
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
// 设置重入次数
setState(c);
return free;
}
5. 读写锁ReentrantReadWriteLock的原理
实际应用中,写少读多的情况很多,而使用ReentranLock性能太低(只有读锁时不需要限制其他读锁).ReentrantReadWriteLock应运而生,采用读写分离的策略,允许多个线程可以同时获取读锁.
(1). 结构
内部维护了一个ReadLock和一个WriteLock,它们都是依赖Sync实现具体功能,而Sunc继承自AQS.AQS中的state只维护了一个状态,而读写锁有两个锁,所以**采用state的高16为表示读锁的状态,低16位表示写锁的状态.**
(2). 写锁的获取与释放
写锁使用WriteLock实现.
1). void lock()
写锁是个可重入的独占锁,当没有线程获取读锁且写锁空闲或者写锁属于当前线程时,可以获取.
public void lock() {
sync.acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// 此方法在WriteLock(内部类)中实现
protected final boolean tryAcquire(int acquires) {
// 获取当前线程,写锁的重入次数W
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
// 写锁或者读锁已经被获取
if (c != 0) {
// w==0证明写锁空闲,那么就是读锁被获取,如果读锁空闲继而判断当前线程是否是写锁中的线程
if (w == 0 || current != getExclusiveOwnerThread())
// 进入这里说明读锁空闲,那么不能获取写锁.或者写锁重入的不是当前线程
return false;
// 走到这证明可以获取读锁没被获取,而且当前线程获取了写锁,这一步判断是否能继续进行重入
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 设置重入值
setState(c + acquires);
return true;
}
// 第一个写线程获取写锁
// writerShouldBlock()当有别的线程也在获取此锁时,是否应该阻塞(分公平和非公平两种实现)
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
// 非公平锁,永远不需要判断,不阻塞,抢就行了
final boolean writerShouldBlock() {
return false; // writers can always barge
}
//公平锁,调用hasQueuedPredecessors()判断是否有前继节点,如果有说明不是队首,继续排队,放弃获取锁
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
2). void lockInterruptibly()
同样是获取锁,此方法会对中断进行响应.调用的是AQS中响应中断的获取锁方法.
3). boolean tryLock()
尝试获取写锁,没有获取到不会阻塞,逻辑同上.
4). boolean tryLock(long timeout, TimeUnit unit)
获取锁失败后挂起指定时间,如果时间到了之后还是没有获取到锁,返回false
5). void unlock()
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
// 上面都是AQS的内容,下面的方法是具体的锁实现的
protected final boolean tryRelease(int releases) {
//判断是否是写锁的拥有者调用的unlock
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 是当前线程获取的写锁
// 获取可重入值,写锁是低16位,可以直接加减.如果写锁重入次数为0,则锁应修改为空闲状态
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
(3). 读锁的获取与释放
读锁是使用ReadLock实现的.
1). void lock()
获取读锁,如果写锁是空闲的,其他线程都可以获取读锁.读锁是共享的
public void lock() {
sync.acquireShared(1);
}
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
// 具体的锁的实现
protected final int tryAcquireShared(int unused) {
// 获取当前线程,读锁标记
Thread current = Thread.currentThread();
int c = getState();
// 判断写锁是否被占用,且占用者不是自己(一个线程拥有了写锁,也是可以拥有读锁的,加锁主要是避免不同线程之间的不同步,在一个线程内一定是安全的)
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// 获取读锁的计数
int r = sharedCount(c);
// 尝试获取锁,只有一个线程可以成功
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARxianchengED_UNIT)) {
// 获取之前读锁是空闲的,也就是说,这个线程是第一个获取读锁的线程
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {// 此线程是之前第一个获取锁的线程
firstReaderHoldCount++;
} else {// 如果不是第一个获取多锁的线程,将该线程持有锁的次数信息,放入线程本地变量中,方便在整个请求上下文(请求锁、释放锁等过程中)使用持有锁次数信息。
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
// 没有获取成功的线程进入fullTryAcquireShared,逻辑与tryAcquireShared()类似,但是是自旋的
return fullTryAcquireShared(current);
}
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
int c = getState();
// 写锁被占用
if (exclusiveCount(c) != 0) {
// 当前线程没有占有读锁
if (getExclusiveOwnerThread() != current)
return -1;
}
// 竞争时应不应该挂起自己
else if (readerShouldBlock()) {
// 此线程是第一个得到读锁的线程
if (firstReader == current) {
// doNothing
} else {
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();
}
}
if (rh.count == 0)
return -1;
}
}
// 是否达到最大共享值
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 获取锁
if (compareAndSetState(c, c + SHARED_UNIT)) {
// 当前锁空闲
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
}
// 如果firstReader是当前线程,或者当前线程的cachedHoldCounter变量的count不为0(表示当前线程已经持有了该共享锁),均说明当前线程已经持有共享锁,此次获取共享锁是重入,这也是允许的,可以通过判断。
// 此处将重入次数分为fistReader的重入次数和其他所有线程的重入次数之和
else if (firstReader == current) {
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}
2). void lockInterruptibly()
可以对中断进行响应的获取锁.
3). boolean tryLock()
尝试获取锁,没有成功不会阻塞.
4). boolean tryLock(long timeout, TimeUnit unit)
如果超时时间内(挂起了)还没有获取锁,返回false.
5). void unlock()
public void unlock() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
// 具体锁实现的逻辑
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
// 对第一个获取锁的线程的重入次数进行更新
if (firstReader == current) {
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
}
// 其他线程重入次数的更新
else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
// 自旋的释放锁
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
// CAS成功则说明当前线程没有被其他线程打扰
if (compareAndSetState(c, nextc))
// 返回值为读锁是否空闲
return nextc == 0;
}
}
6. JDK 8 中新增的StampedLock锁探究
(1). 概述
StampedLock锁是并发包中JDK 8版本新增的一个锁,提供了三种模式的读写控制.当调用try系列方法尝试获取锁时,会返回一个long型的变量stamp(戳记).这个戳记代表了锁的状态,可以理解为乐观锁中的版本.释放锁的转换锁时都需要提供这个标记来判断权限.
特点:
- 所有获取锁的方法都会返回一个stamp戳记,0为失败,其他为获取成功
- 所有释放锁的方法都需要一个stamp戳记,必须和之前得到锁时的stamp一致
- 不可重入
- 有三种访问模式
- 乐观读
- 悲观读
- 写锁
- 写锁可以降级为读锁
- 都不支持条件队列
/** 等待链表队列,每一个WNode标识一个等待线程 */
static final class WNode {
volatile WNode prev;
volatile WNode next;
volatile WNode cowait; // 读模式使用该节点形成栈
volatile Thread thread; // non-null while possibly parked
volatile int status; // 0, WAITING, or CANCELLED
final int mode; // RMODE or WMODE
WNode(int m, WNode p) { mode = m; prev = p; }
}
/** 锁队列状态, 当处于写模式时第8位为1,读模式时前7为为1-126(读锁是共享锁,附加的readerOverflow用于当读者超过126时) */
private transient volatile long state;
/** 将state超过 RFULL=126的值放到readerOverflow字段中 */
private transient int readerOverflow;
(2). 写锁writeLock
独占锁,不可重入.请求该锁成功后会返回一个stamp戳记变量来表示该锁的版本.
- writeLock():当完全没有加锁时,绕过acquireWrite,否则调用acquireWrite入队列获取锁资源,
- acquireWrite():入队自旋,并放到队列尾部,如果队列中只剩下一个结点,则在队头进一步自旋,最后会进入阻塞
- unlockWrite():如果锁的状态与stamp相同,调用release()释放锁
- release():唤醒传入节点的后继节点
(3). 悲观读readLock
共享锁,不可重入
- readLock():(队列为空&&没有写锁同时读锁数小于126&&CAS修改状态成功)则状态加1并返回,否则调用acquireRead()自旋获取读锁
- acquireRead():首先是入队自旋,如果队尾不是读模式则放到队列尾部,如果是读模式,则放到队尾的cowait中。如果队列中只剩下一个结点,则在队头进一步自旋.如果最终依然失败,则Unsafe().park()挂起当前线程。
- unlockRead():如果state匹配stamp,判断当前的共享次数,修改state或者readerOverflow
(4). 乐观读OptimisticRead()
在操作数据前并没有通过 CAS 设置锁的状态,仅仅是通过位运算测试
如果当前没有线程持有写锁,则简单的返回一个非 0 的 stamp 版本信息,获取该 stamp 后在具体操作数据前还需要调用 validate 验证下该 stamp 是否已经不可用,也就是看当调用 tryOptimisticRead 返回 stamp 后,到当前时间是否有其它线程持有了写锁,如果是那么 validate 会返回 0,否则就可以使用该 stamp 版本的锁对数据进行操作。
由于 tryOptimisticRead 并没有使用 CAS 设置锁状态,所以不需要显示的释放该锁。该锁的一个特点是适用于读多写少的场景,因为获取读锁只是使用位操作进行检验,不涉及 CAS 操作,所以效率会高很多,但是同时由于没有使用真正的锁,在保证数据一致性上需要拷贝一份要操作的变量到方法栈,并且在操作数据时候可能其它写线程已经修改了数据,而我们操作的是方法栈里面的数据,也就是一个快照,所以最多返回的不是最新的数据,但是一致性还是得到保障的。
/**
* 获取乐观读锁,返回邮票stamp
*/
public long tryOptimisticRead() {
long s; //有写锁返回0. 否则返回256
return (((s = state) & WBIT) == 0L) ? (s & SBITS) : 0L;
}
/**
* 验证从调用tryOptimisticRead开始到现在这段时间内有无写锁占用过锁资源,有写锁获得过锁资源则返回false. stamp为0返回false.
* @return 从返回stamp开始,没有写锁获得过锁资源返回true,否则返回false
*/
public boolean validate(long stamp) {
//强制读取操作和验证操作在一些情况下的内存排序问题
U.loadFence();
//当持有写锁后再释放写锁,该校验也不成立,返回false
return (stamp & SBITS) == (state & SBITS);
}
(5). 锁转换
/**
* state匹配stamp时, 执行下列操作之一.
* 1、stamp 已经持有写锁,直接返回.
* 2、读模式,但是没有更多的读取者,并返回一个写锁stamp.
* 3、有一个乐观读锁,只在即时可用的前提下返回一个写锁stamp
* 4、其他情况都返回0
*/
public long tryConvertToWriteLock(long stamp) {
long a = stamp & ABITS, m, s, next;
//state匹配stamp
while (((s = state) & SBITS) == (stamp & SBITS)) {
//没有锁
if ((m = s & ABITS) == 0L) {
if (a != 0L)
break;
//CAS修改状态为持有写锁,并返回
if (U.compareAndSwapLong(this, STATE, s, next = s + WBIT))
return next;
}
//持有写锁
else if (m == WBIT) {
if (a != m)
//其他线程持有写锁
break;
//当前线程已经持有写锁
return stamp;
}
//有一个读锁
else if (m == RUNIT && a != 0L) {
//释放读锁,并尝试持有写锁
if (U.compareAndSwapLong(this, STATE, s, next = s - RUNIT + WBIT))
return next;
}else
break;
}
return 0L;
}
/**
* state匹配stamp时, 执行下列操作之一.
1、stamp 表示持有写锁,释放写锁,并持有读锁
2 stamp 表示持有读锁 ,返回该读锁
3 有一个乐观读锁,只在即时可用的前提下返回一个读锁stamp
4、其他情况都返回0,表示失败
*
*/
public long tryConvertToReadLock(long stamp) {
long a = stamp & ABITS, m, s, next; WNode h;
//state匹配stamp
while (((s = state) & SBITS) == (stamp & SBITS)) {
//没有锁
if ((m = s & ABITS) == 0L) {
if (a != 0L)
break;
else if (m < RFULL) {
if (U.compareAndSwapLong(this, STATE, s, next = s + RUNIT))
return next;
}
else if ((next = tryIncReaderOverflow(s)) != 0L)
return next;
}
//写锁
else if (m == WBIT) {
//非当前线程持有写锁
if (a != m)
break;
//释放写锁持有读锁
state = next = s + (WBIT + RUNIT);
if ((h = whead) != null && h.status != 0)
release(h);
return next;
}
//持有读锁
else if (a != 0L && a < WBIT)
return stamp;
else
break;
}
return 0L;
}
(6). 使用乐观读锁
使用乐观读要保证以下顺序:
// 获取版本信息(乐观锁)
long stamp = lock.tryOptimisticRead();
// 复制变量到本地堆栈
copyVaraibale2ThreadMemory();
// 校验,如果校验失败,说明此处乐观锁使用失败,申请悲观读锁
if(!lock.validate(stamp)){
// 申请悲观读锁
long stamp = lock.readLock();
try{
// 复制变量到本地堆栈
copyVaraibale2ThreadMemory();
}finally{
// 使用完之后释放锁
lock.unlock();
}
}