什么是JUC
Java.util.concurrent
工具包的简称,这个包下都是Java处理线程相关的类,自jdk1.5
后出现。里边提供了各种各样的控制同步和线程通信的工具类。
JUC
出现的原因
一个应用程序想要操作硬件呢就需要先搞个驱动程序,由驱动程序控制硬件做些事情,驱动程序再由操作系统控制做些事情,编程语言想要操作操作系统,肯定不能让编程语言直接操作操作系统,不然容易把操作系统搞没,这时就需要操作系统提供接口层来供编程语言使用,但是操作系统变了接口层也需要跟着变,就需要操作系统提供一个抽象层来供用户实现接口的调用,并且提供简单的功能,Glibc
函数库就实现了这样的操作,最后再由应用程序选取哪个语言进行操作。
在这个过程中,每个语言都有自己的库,来支撑语言沟通操作系统,Java
没有锁、原子类等工具的支撑,而这时JUC
的出现为Java
语言提供多线程同步结构的支撑。
JUC
解决了什么问题
用于解决多线程同步问题,用于给Java开发者提供便利的函数、功能以及结构。
锁分析
我们知道一个Java
方法是由一行行指令序列组成的,多线程想要修改内存的count
值,如何保证线程安全呢,CPU
底层又是如何实现的呢
我们可以通过CAS
自旋加阻塞队列实现。CAS
自旋的意思就是假如两个CPU
要想修改内存的count
值,会在自己内部保存两个变量值,一个是修改前的,一个是修改后的,如果修改前的值和内存值对的上就进行修改,如果对不上,那么就一直自旋读取。我们了解了自旋的大致流程,如果想要保证线程安全,只能由一个CPU
获取到锁,其它CPU
自旋等待获取到锁的CPU
执行完操作释放锁,这时就会有个问题,就是CPU
一直获取不到锁,一直自旋占用资源,那么就把它放到阻塞队列中。
上面分析了如何安全修改值,那么接下来聊聊自旋锁和阻塞怎么选择
如果有线程和CPU
无限多,就会导致很多个CPU
一直拿不到锁,一直陷入自旋中,占用资源就需要陷入阻塞中,就好比上厕所,有很多个人,只能一个人上,就会导致其他人上不了,不能让他们在那一直干等着,这样不合适,就不适合使用自旋,就需用到阻塞。
总结
当自旋锁消耗CPU的时间大于线程阻塞线程切换上下文的时间时间时选用阻塞而非自旋
自旋锁
缺点:CPU
占用不干事,导致性能障碍,简称占着茅坑不拉屎
优点:适用于执行步骤少很快能完成的操作,自旋一会儿马上就能获取锁,这样不会消耗太多CPU
资源
注意:当CPU
个数增加且线程数增加的情况下,优点会退化成自旋锁的缺点,就好比上厕所,如果只有两个人的时候,另一个人排队等下很快就轮到了,但是人一多就不行了
使用场景:征用较少且代码量小的 临界区
如何实现一把锁
1.如何表示锁状态:无锁,有锁
boolean state
;//true
有锁false
无锁 只能表示两种状态
为了实现锁重入,那么我需要记录锁重入次数,这样boolean
类型不合适,可以用int
类型。
0无锁,大于0重入次数 比如1代表重入1次
2.如何保证多线程钱锁线程安全
CAS
3.如何处理获取不到锁的线程
自旋 阻塞
4.如何释放锁
自旋:自己抢锁
阻塞:唤醒
聊了如何实现一把锁,接下来AQS原理大致结构就跟上面差不多,接下来了解下AQS
是什么吧
锁机制原理
LockSupport
原理
用于支撑JUC
包用于操作线程阻塞和唤醒的工具类。可以看到底层都是通过Unsafe类来支撑。
public class LockSupport {
private LockSupport() {}
// 设置造成线程阻塞的对象信息,用于:debug、jstack
private static void setBlocker(Thread t, Object arg) {
UNSAFE.putObject(t, parkBlockerOffset, arg); // 去除CAS的volatile优化
}
// 唤醒当前阻塞的线程对象
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
// 带有提示对象blocker的阻塞线程操作
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}
// 带超时时间(绝对时间)和提示对象的阻塞线程操作
public static void parkNanos(Object blocker, long nanos) {
if (nanos > 0) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, nanos);
setBlocker(t, null);
}
}
// 带超时时间(相对时间)和提示对象的阻塞线程操作
public static void parkUntil(Object blocker, long deadline) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(true, deadline);
setBlocker(t, null);
}
// 获取阻塞对象
public static Object getBlocker(Thread t) {
if (t == null)
throw new NullPointerException();
return UNSAFE.getObjectVolatile(t, parkBlockerOffset);
}
public static void park() {
UNSAFE.park(false, 0L);
}
public static void parkUntil(long deadline) {
UNSAFE.park(true, deadline);
}
static final int nextSecondarySeed() {
int r;
Thread t = Thread.currentThread();
if ((r = UNSAFE.getInt(t, SECONDARY)) != 0) {
r ^= r << 13; // xorshift
r ^= r >>> 17;
r ^= r << 5;
}
else if ((r = java.util.concurrent.ThreadLocalRandom.current().nextInt()) == 0)
r = 1; // avoid zero
UNSAFE.putInt(t, SECONDARY, r);
return r;
}
// 获取unsafe需要的地址值
private static final sun.misc.Unsafe UNSAFE;
private static final long parkBlockerOffset;
private static final long SEED;
private static final long PROBE;
private static final long SECONDARY;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> tk = Thread.class;
parkBlockerOffset = UNSAFE.objectFieldOffset
(tk.getDeclaredField("parkBlocker"));
SEED = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomSeed"));
PROBE = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomProbe"));
SECONDARY = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomSecondarySeed"));
} catch (Exception ex) { throw new Error(ex); }
}
}
AQS
原理
Abstract
:因为它并不知道怎么对state变量怎么操作进行上锁,由子类进行设计。模板方法设计模式即可,暴露出上锁逻辑
Queue
:线程阻塞队列
Synchronizer
:同步
CAS+state
完成多线程抢锁逻辑
Queue
完成抢不到锁的线程排队
AQS
核心代码
acquire()
获取互斥锁(写锁)
获取写锁(互斥锁)的代码。tryAcquire
方法由子类来完成,该方法也称之为模板方法,为何如此设计?这时因为AQS无法知道子类如何定义获取锁的操作。假如子类判断当前线程没有获取到锁,那么调用addWaiter(Node.EXCLUSIVE)
方法用于排队占位,acquireQueued
方法用于占位后要不要去进行等待,此时不难猜出这里面肯定调用了tryAcquire(arg)
,可以想想为什么?因为在任何过程中,都有可能别的线程已经释放了锁。
public final void acquire(int arg) {
if (!tryAcquire(arg) && // 子类判定获取锁失败返回false,那么这里取反,表示为 true
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 获取失败后添加到阻塞队列
selfInterrupt();
}
// 子类实现获取锁的逻辑,AQS并不知道你怎么用这个state来上锁
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
addWaiter()
采用全路径+优化前置的技巧,实现快速入队。
全路径enq()
如果需要保证多线程插入安全,就需要保证指针移动安全,也就是上述三个步骤是原子性,但是上锁的话就会导致效率很低,有没有什么简单的方法呢,可以通过CAS的操作保证线程安全,首先用一个变量记录tail的位置,先让线程1的prev指向节点1,然后CAS更新tail位置,最后把节点1的位置校验正确。
private Node addWaiter(Node mode) {
// 创建等待节点:当前线程对象 + 锁模式(互斥锁?共享锁)
Node node = new Node(Thread.currentThread(), mode);
// 快速加入到队列
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
// (面试重点,在后面进行操作时,最难理解,还有特重点在这里)特别注意:当上面的CAS成功后,有一瞬间 这里的pred.next并没有关联。会导致什么问题?有一瞬间,你通过head引用遍历的时候,是到达不了最后一个节点的,A(head) <-> B(旧tail) <- C(tail)。如何获取最新的节点呢?通过tail指针往前遍历即可
pred.next = node;
return node;
}
}
enq(node);
return node;
}
// 全路径入队方法
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // 懒加载时,head和tail 分别代表aqs的头尾节点
// 通过CAS实现原子初始化操作,直接用一个空节点实现初始化,此时head和tail指向同一个空节点
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
acquireQueued
方法
当加入阻塞队列后,调用该方法考虑是否将当前线程进行阻塞。在看该方法时,请考虑一个情况:假如在添加到阻塞队列后,当前锁状态是无锁时, 怎么办?那么一定时尝试获取锁。
// node节点为阻塞队列中的线程节点
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 当前节点的前驱节点
final Node p = node.predecessor();
// 假如p是头结点,有可能此时头结点释放了锁,那么尝试调用tryAcquire,让子类抢一下锁
if (p == head && tryAcquire(arg)) {
// 获取成功,更新头结点,释放引用
setHead(node);
p.next = null;
failed = false;
return interrupted;
}
// 如果前驱节点不是头结点或者抢锁失败,何如?那就先判断是否应该被阻塞,如果阻塞呢?调用parkAndCheckInterrupt方法来阻塞当前线程。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
// 阻塞当前线程。响应中断的方式阻塞线程
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); // this是啥意思?用于指明当前线程阻塞在哪个对象上,后期可以通过jstack命令来看到,用于排除问题
return Thread.interrupted(); // 判断是否是通过中断的方式来唤醒的。1、unpark 2、interrupt
}
shouldParkAfterFailedAcquire
方法
该方法用于判断当前线程节点是否应该阻塞。无非就是找到一个可靠(活着有效)的节点,然后将当前线程节点作为其后继节点即可。
// pred是前驱节点,node是当前线程节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// 如果前驱节点是SIGNAL,那么此时可以安全的睡眠(因为SIGNAL状态,代表了上一个线程是活的,它可以通知你,所以当前线程节点可以安全的阻塞了)
return true;
if (ws > 0) {
// 前一个节点是CANCEL无效节点,那咋整?一直往前找,直到找到(有效节点)
do {
// 前驱节点的前驱节点,也即:踩着死亡节点往前跳
Node predPrev = pred.prev;
// 将前驱的前驱当成当前线程节点的前驱节点
pred = predPrev;
// 更新引用
node.prev = pred;
} while (pred.waitStatus > 0);
// 更新找到的前驱有效节点的next引用,指向当前节点
pred.next = node;
} else {
// 正常节点,那么CAS 将其变为SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
Node内部类(只介绍出现过的某些变量)
该内部类用于作用AQS的阻塞队列,封装线程节点。
static final class Node {
// 标志当前线程节点阻塞状态为:共享节点
static final Node SHARED = new Node();
// 标志当前线程节点的阻塞状态为:互斥节点
static final Node EXCLUSIVE = null;
// 唯一的大于0的节点,表示当前节点已经被取消,属于无效节点
static final int CANCELLED = 1;
// 表明当前节点是活动节点,可以唤醒后继的等待节点
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
// 用于共享锁唤醒后继节点标志位用
static final int PROPAGATE = -3;
// 代表了当前节点的状态值,简称ws,取值为以上4个
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
final boolean isShared() {
return nextWaiter == SHARED;
}
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
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;
}
// 子类实现释放锁的逻辑,AQS并不知道你怎么用这个state来释放锁
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
unparkSuccessor
方法
该方法用于释放node节点后的有效后继节点。说白了,就是从node节点往后找到有效地节点,唤醒即可。
private void unparkSuccessor(Node node) {
// 开始唤醒后继节点,当前头节点,且ws=SINGAL,CAS将其变为0,代表了我当前已经响应了这一次的唤醒操作
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 取当前头结点的后继节点,作为唤醒节点,但是,请注意条件
Node s = node.next;
if (s == null || // 为什么这里有可能为空?因为我们首先更新的是tail引用,然后再是旧的tail.next所以有可能一瞬间为空
s.waitStatus > 0) { // 后继节点居然是无效节点???
s = null;
// tail引用一定是最新的引用,那么从后往前找到 第一个(node节点的后继的第一个) 有效节点
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 找到了s节点,此时s就是要唤醒的节点
if (s != null)
LockSupport.unpark(s.thread);
}
acquireShared
方法
该方法用于获取共享锁。
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0) // 由子类判断是否获取锁成功,若不成功?该阻塞阻塞去
doAcquireShared(arg);
}
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
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);
// 如果获取锁成功,尝试唤醒后面的共享节点(因为共享锁是可以多线程同时获取,参考下:读写锁实现的读锁)
// A(head 写)->B(读)->C(读)
if (r >= 0) {
setHeadAndPropagate(node, r); // 将当前获取锁的共享节点更新头部,然后唤醒后继节点
p.next = null; // help GC
if (interrupted) // 如果在等待过程中发生了中断,由于中断而被唤醒,那么置位当前线程的中断标志位
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
// 用于更新头结点,并且唤醒后继共享节点(面试重点,难点)
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
if (propagate > 0 || // 信号量还有多余的,那么直接唤醒。eg:A:2 head->B-C
h == null || // 不可能发生
h.waitStatus < 0 || // SIGNAL 表明必须唤醒后继节点 那么直接唤醒
(h = head) == null || // 不可能发生。A B (获取锁) head -> D
h.waitStatus < 0) { // SIGNAL 表明必须唤醒后继节点 那么直接唤醒
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
// 释放共享锁节点。(难点,核心点,面试点)。eg:当前状态:A B (获取锁) head -> C -> D
private void doReleaseShared() {
for (;;) {
Node h = head; // 保存head临时变量(两个状态:head没有被更新、head被更新)
if (h != null && h != tail) { // 链表中还存在等待节点
int ws = h.waitStatus;
// 理应要唤醒后面的节点,因为SIGNAL的语义就是必须唤醒后面的节点(正常状态)
if (ws == Node.SIGNAL) {
// CAS将该状态由SIGNAL改为0,表示唤醒成功,那么如果失败呢?此时表明多个线程同时释放了共享锁,都需要同时唤醒后继节点,此时state 状态为 2 表明了资源数可用为2,需要唤醒后面两个等待的共享节点
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
// 直接唤醒后继节点即可
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) // PROPAGATE = -3 若CAS成功,表明A释放了一个共享锁时唤醒了C,此时在C还没有更新头节点之前,A马上又释放了一个共享锁,且CAS成功将头结点的状态改为了PROPAGATE,此时完美保证了第一个状态唤醒的正确性
continue;
}
if (h == head) // 头结点至今没被改变过,且 h != null && h != tail 成功,那么直接退出循环,没有节点了
break;
}
}
cancelAcquire
方法
取消的这个在竞争队列上的节点有几种状态:
- 在队列尾部
- 在队列中间
- 在队列前面,头结点的后面
private void cancelAcquire(Node node) {
if (node == null)
return;
node.thread = null;
// 找到一个合适的前驱节点(未被cancel的节点)
Node pred = node.prev;
while (pred.waitStatus > 0) // 也即ws不大于 cancel的状态 1
node.prev = pred = pred.prev;
// 有效前驱节点的后继节点
Node predNext = pred.next;
// 标识节点是无效节点,此时其他线程将会看到该状态,同时越过该节点处理
node.waitStatus = Node.CANCELLED;
if (node == tail && // 当前删除的节点 可能(删除时可以并发插入) 是尾结点
compareAndSetTail(node, pred)) { // 若当前快照看到了是尾结点,那么CAS尝试删除(修改tail指针为pred)
compareAndSetNext(pred, predNext, null); // cas修改next指针
} else {
int ws;
if (pred != head && // 被取消的节点不在头结点的后面
((ws = pred.waitStatus) == Node.SIGNAL || // pred需要唤醒后继节点
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) { // 线程对象必须不为空,因为只有线程对象存在的情况下该节点才是最终有效节点
Node next = node.next;
// 当前被删除节点的后继节点必须时有效节点
if (next != null && next.waitStatus <= 0)
// A->B(CANCEL)->C A->C
compareAndSetNext(pred, predNext, next); // 尝试修改pred前驱节点的next指向node的后继节点
} else {
// 唤醒后继节点即可
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
ConditionObject
原理
核心变量定义与构造器原理
根据变量定义,公用AQS的Node节点。
public interface Condition {
void await() throws InterruptedException;
void awaitUninterruptibly();
boolean await(long time, TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
void signal();
void signalAll();
}
public class ConditionObject implements Condition {
/** 条件阻塞队列的头结点 */
private transient Node firstWaiter;
/** 条件阻塞队列的尾结点 */
private transient Node lastWaiter;
}
await()
前置分析
持有锁的线程在执行await方法后会做几个操作:
- 判断线程是否中断,如果中断了,什么都不做。
- 没有中断,就讲当前线程封装为Node添加到Condition的单向链表中
- 一次性释放掉锁资源。
- 如果当前线程没有在AQS队列,就正常执行LockSupport.park(this)挂起线程。
// await方法的前置分析,只分析到线程挂起
public final void await() throws InterruptedException {
// 先判断线程的中断标记位是否是true
if (Thread.interrupted())
// 如果是true,就没必要执行后续操作挂起了。
throw new InterruptedException();
// 在线程挂起之前,先将当前线程封装为Node,并且添加到Condition队列中
Node node = addConditionWaiter();
// fullyRelease在释放锁资源,一次性将锁资源全部释放,并且保留重入的次数
int savedState = fullyRelease(node);
// 省略一行代码……
// 当前Node是否在AQS队列中?
// 执行fullyRelease方法后,线程就释放锁资源了,如果线程刚刚释放锁资源,其他线程就立即执行了signal方法,
// 此时当前线程就被放到了AQS的队列中,这样一来线程就不需要执行LockSupport.park(this);去挂起线程了
while (!isOnSyncQueue(node)) {
// 如果没有在AQS队列中,正常在Condition单向链表里,正常挂起线程。
LockSupport.park(this);
// 省略部分代码……
}
// 省略部分代码……
}
addConditionWaiter()
private Node addConditionWaiter() {
// 拿到尾节点。
Node t = lastWaiter;
// 如果尾节点有值,并且尾节点的状态不正常,不是-2,尾节点可能要拜拜了~
if (t != null && t.waitStatus != Node.CONDITION) {
// 如果尾节点已经取消了,需要干掉取消的尾节点~
unlinkCancelledWaiters();
// 重新获取lastWaiter
t = lastWaiter;
}
// 构建当前线程的Node,并且状态设置为-2
Node node = new Node(Thread.currentThread(), Node.CONDITION);
// 如果last节点为null。直接将当前节点设置为firstWaiter
if (t == null)
firstWaiter = node;
else
// 如果last节点不为null,说明有值,就排在lastWaiter的后面
t.nextWaiter = node;
// 把当前节点设置为最后一个节点
lastWaiter = node;
// 返回当前节点
return node;
}
unlinkCancelledWaiters
方法原理
private void unlinkCancelledWaiters() {
// 拿到头节点
Node t = firstWaiter;
// 声明一个节点,爱啥啥~~~
Node trail = null;
// 如果t不为null,就正常执行~~
while (t != null) {
// 拿到t的next节点
Node next = t.nextWaiter;
// 如果t的状态不为-2,说明有问题
if (t.waitStatus != Node.CONDITION) {
// t节点的next为null
t.nextWaiter = null;
// 如果trail为null,代表头结点状态就是1,
if (trail == null)
// 将头结点指向next节点
firstWaiter = next;
else
// 如果trail有值,说明不是头结点位置
trail.nextWaiter = next;
// 如果next为null,说明单向链表遍历到最后了,直接结束
if (next == null)
lastWaiter = trail;
}
// 如果t的状态是-2,一切正常
else {
// 临时存储t
trail = t;
}
// t指向之前的next
t = next;
}
}
fullyRelease
方法原理
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;
}
}
signal()
分析
分为了几个部分:
- 确保执行signal方法的是持有锁的线程
- 脱离Condition的队列
- 将Node状态从-2改为0
- 将Node添加到AQS队列
- 为了避免当前Node无法在AQS队列正常唤醒做了一些判断和操作
// 线程挂起后,可以基于signal唤醒~
public final void signal() {
// 在ReentrantLock中,如果执行signal的线程没有持有锁资源,直接扔异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 拿到排在Condition首位的Node
Node first = firstWaiter;
// 有Node在排队,才需要唤醒,如果没有,直接告辞~~
if (first != null)
doSignal(first);
}
doSignal()
// 开始唤醒Condition中的Node中的线程
private void doSignal(Node first) {
// 先一波do-while走你~~~
do {
// 获取到第二个节点,并且将第二个节点设置为firstWaiter
if ( (firstWaiter = first.nextWaiter) == null)
// 说明就一个节点在Condition队列中,那么直接将firstWaiter和lastWaiter置位null
lastWaiter = null;
// 如果还有nextWaiter节点,因为当前节点要被唤醒了,脱离整个Condition队列。将nextWaiter置位null
first.nextWaiter = null;
// 如果transferForSignal返回true,一切正常,退出while循环
} while (!transferForSignal(first) &&
// 如果后续节点还有,往后面继续唤醒,如果没有,退出while循环
(first = firstWaiter) != null);
}
transferForSignal()
// 准备开始唤醒在Condition中排队的Node
final boolean transferForSignal(Node node) {
// 将在Condition队列中的Node的状态从-2,改为0,代表要扔到AQS队列了。
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
// 如果失败了,说明在signal之前应当是线程被中断了,从而被唤醒了。
return false;
// 如果正常的将Node的状态从-2改为0,这是就要将Condition中的这个Node扔到AQS的队列。
// 将当前Node扔到AQS队列,返回的p是当前Node的prev
Node p = enq(node);
// 获取上一个Node的状态
int ws = p.waitStatus;
// 如果ws > 0 ,说明这个Node已经被取消了。
// 如果ws状态不是取消,将prev节点的状态改为-1,。
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
// 如果prev节点已经取消了,可能会导致当前节点永远无法被唤醒。立即唤醒当前节点,基于acquireQueued方法,
// 让当前节点找到一个正常的prev节点,并挂起线程
// 如果prev节点正常,但是CAS修改prev节点失败了。证明prev节点因为并发原因导致状态改变。还是为了避免当前
// 节点无法被正常唤醒,提前唤醒当前线程,基于acquireQueued方法,让当前节点找到一个正常的prev节点,并挂起线程
LockSupport.unpark(node.thread);
// 返回true
return true;
}
await()
后置分析
分为了几个部分:
- 唤醒之后,要先确认是中断唤醒还是signal唤醒,还是signal唤醒后被中断
- 确保当前线程的Node已经在AQS队列中
- 执行acquireQueued方法,等待锁资源。
- 在获取锁资源后,要确认是否在获取锁资源的阶段被中断过,如果被中断过,并且不是THROW_IE,那就确保interruptMode是REINTERRUPT。
- 确认当前Node已经不在Condition队列中了
- 最终根据interruptMode来决定具体做的事情
- 0:嘛也不做。
- THROW_IE:抛出异常
- REINTERRUPT:执行线程的interrupt方法
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);
// 如果线程执行到这,说明现在被唤醒了。
// 线程可以被signal唤醒。(如果是signal唤醒,可以确认线程已经在AQS队列中)
// 线程可以被interrupt唤醒,线程被唤醒后,没有在AQS队列中。
// 如果线程先被signal唤醒,然后线程中断了。。。。(做一些额外处理)
// checkInterruptWhileWaiting可以确认当前中如何唤醒的。
// 返回的值,有三种
// 0:正常signal唤醒,没别的事(不知道Node是否在AQS队列)
// THROW_IE(-1):中断唤醒,并且可以确保在AQS队列
// REINTERRUPT(1):signal唤醒,但是线程被中断了,并且可以确保在AQS队列
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// Node一定在AQS队列
// 执行acquireQueued,尝试在ReentrantLock中获取锁资源。
// acquireQueued方法返回true:代表线程在AQS队列中挂起时,被中断过
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
// 如果线程在AQS队列排队时,被中断了,并且不是THROW_IE状态,确保线程的interruptMode是REINTERRUPT
// REINTERRUPT:await不是中断唤醒,但是后续被中断过!!!
interruptMode = REINTERRUPT;
// 如果当前Node还在condition的单向链表中,脱离Condition的单向链表
if (node.nextWaiter != null)
unlinkCancelledWaiters();
// 如果interruptMode是0,说明线程在signal后以及持有锁的过程中,没被中断过,什么事都不做!
if (interruptMode != 0)
// 如果不是0~
reportInterruptAfterWait(interruptMode);
}
isOnSyncQueue
方法原理
final boolean isOnSyncQueue(Node node) {
// waitStatus等于CONDITION一定在条件阻塞队列上,prev为null,一定不在竞争队列上?因为竞争队列上的节点prev一定不为空,且prev节点的状态可能为SIGNAL。
// 有同学说:头结点的prev为null,那么我问你:头结点在等待队列上吗?头结点代表了什么?代表了假节点,仅仅只是为了保证获取到后继的节点而设立的,
// 如果硬要说有意义,那么一定是代表了当前获取到了锁的节点
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
// 若节点的next节点不为空,那么一定在AQS 竞争队列上
if (node.next != null)
return true;
// 如果上述判断都没有确认节点在AQS队列上,在AQS队列中寻找一波
return findNodeFromTail(node);
}
private boolean findNodeFromTail(Node node) {
Node t = tail;
// 从尾指针开始遍历往前遍历直到找到该节点为止
for (;;) {
if (t == node)
return true;
if (t == null)
return false;
t = t.prev;
}
}
checkInterruptWhileWaiting
方法
其实特殊处理的就是:线程被中断返回,而不是正常unpark
返回。如果正常返回该方法返回0。
private int checkInterruptWhileWaiting(Node node) {
return Thread.interrupted() ? (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) : 0;
}
final boolean transferAfterCancelledWait(Node node) {
// 基于CAS将Node的状态从-2改为0
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
// 说明是中断唤醒的线程。因为CAS成功了。
// 将Node添加到AQS队列中~(如果是中断唤醒的,当前线程同时存在Condition的单向链表以及AQS的队列中)
enq(node);
// 返回true
return true;
}
// 判断当前的Node是否在AQS队列(signal唤醒的,但是可能线程还没放到AQS队列)
// 等到signal方法将线程的Node扔到AQS队列后,再做后续操作
while (!isOnSyncQueue(node))
// 如果没在AQS队列上,那就线程让步,稍等一会,Node放到AQS队列再处理(看CPU)
Thread.yield();
// signal唤醒的,返回false
return false;
}
awaitNanos()
awaitNanos:仅仅是在await方法的基础上,做了一内内的改变,整体的逻辑思想都是一样的。
挂起线程时,传入要阻塞的时间,时间到了,自动唤醒,走添加到AQS队列的逻辑
public final long awaitNanos(long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
// deadline:当前线程最多挂起到什么时间点
final long deadline = System.nanoTime() + nanosTimeout;
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
// nanosTimeout的时间小于等于0,直接告辞!!
if (nanosTimeout <= 0L) {
// 正常扔到AQS队列
transferAfterCancelledWait(node);
break;
}
// nanosTimeout的时间大于1000纳秒时,才可以挂起线程
if (nanosTimeout >= spinForTimeoutThreshold)
// 如果大于,正常挂起
LockSupport.parkNanos(this, nanosTimeout);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
// 计算剩余的挂起时间,可能需要重新的走while循环,再次挂起线程
nanosTimeout = deadline - System.nanoTime();
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null)
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
// 剩余的挂起时间
return deadline - System.nanoTime();
}
siginAll()
// 以do-while的形式,将Condition单向链表中的所有Node,全部唤醒并扔到AQS队列
private void doSignalAll(Node first) {
// 将头尾都置位null~
lastWaiter = firstWaiter = null;
do {
// 拿到next节点的引用
Node next = first.nextWaiter;
// 断开当前Node的nextWaiter
first.nextWaiter = null;
// 修改Node状态,扔AQS队列,是否唤醒!
transferForSignal(first);
// 指向下一个节点
first = next;
} while (first != null);
}
非公平锁比公平锁效率要快
因为公平锁需要排队,排队需要一定时间,唤醒线程,线程上下文切换加到运行队列有个延迟过程需要一定时间,线程需要先转换为running
的就绪状态丢到队列里面去,再根据优先级和调度算法才能完成执行。
总结
子类只需要实现自己获取和释放锁的逻辑,至于排队放入阻塞队列、唤醒机制等都由AQS
来实现
ReentrantLock原理
基于AQS
实现的可重入锁实现类
核心变量、构造方法和内部类
public class ReentrantLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = 7373984872572414699L;
// 公平锁和非公平锁实现它
private final Sync sync;
public ReentrantLock() {
// 默认为非公平锁。为何默认为非公平锁?因为通过大量测试下来,发现非公平锁的性能优于公平锁
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
// fair变量选择锁类型(公平锁或非公平锁)
sync = fair ? new FairSync() : new NonfairSync();
}
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
// 子类负责实现
abstract void lock();
// 非公平锁标准获取锁方法
@ReservedStackAccess
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程对象
final Thread current = Thread.currentThread();
int c = getState();
// 代表获取所得线程刚好释放锁,可以进行抢锁
if (c == 0) {
// 继续抢锁,不管阻塞队列
if (compareAndSetState(0, acquires)) {
// 抢锁成功,设置当前线程已获取到锁
setExclusiveOwnerThread(current);
return true;
}
}
// 当前线程就是持有锁的线程,看看有没有发生锁重入
else if (current == getExclusiveOwnerThread()) {
// 利用state变量记录重入次数
int nextc = c + acquires;
// 如果超过了int表示范围,表明符号溢出,所以抛出异常0111 1111 + 1 = 1000 0000
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// 表明抢锁失败,需要AQS把线程放入到阻塞队列中,然后进行阻塞操作等待线程唤醒获取锁
return false;
}
// 公平锁和非公平锁标准释放锁公用方法,因为释放锁的时候不用管不管公平
@ReservedStackAccess
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
// 如果当前线程不是上锁的线程
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 不是重入锁,那么当前线程一定是释放锁了,然后我们把当前AQS用于保存当前锁对象的变量ExclusiveOwnerThread设置为null,表明释放锁成功
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
// 注意:此时state全局变量没有改变,也就意味着在setState之前,没有别的线程能够获取锁,这时保证了以上的操作原子性
setState(c);
// // 告诉AQS,我当前释放锁成功了,你可以去唤醒正在等待锁的线程了
return free;
}
protected final boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}
final ConditionObject newCondition() {
return new ConditionObject();
}
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
@ReservedStackAccess
final void lock() {
// 非公平锁直接进行抢锁,不管阻塞队列有无线程
if (compareAndSetState(0, 1))
// 抢锁成功,标识当前线程持有锁
setExclusiveOwnerThread(Thread.currentThread());
else
// 抢锁失败,进入AQS的获取锁流程
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
// 使用父类Sync提供的方法非公平方式来获取锁
return nonfairTryAcquire(acquires);
}
}
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
@ReservedStackAccess
protected final boolean tryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { // 此时有可能正好获取锁的线程释放了锁,也有可能本身就没有线程获取锁
if (!hasQueuedPredecessors() && // 注意:这里和非公平锁的区别在于:hasQueuedPredecessors看看队列中是否有线程正在排队,没有的话再通过CAS抢锁
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;
}
// 返回false 表明需要AQS来将当前线程放入阻塞队列,然后进行阻塞操作等待唤醒获取锁
return false;
}
}
}
核心方法
上锁
public void lock() {
sync.lock();
}
释放锁
public void unlock() {
sync.release(1);
}
总结
公平锁就是不用看阻塞队列有没有线程,直接进行抢锁;
非公平锁需要看阻塞队列有无线程在排队,没有的话进行抢锁。
ReentrantReadWriteLock
介绍和分析
将原来的互斥锁,切割为两把锁:读锁+写锁,为什么切割?三个线程A、B、C,其中A和B为读取变量的线程,而C为修改变量的线程,且C很少去修改变量,此时为什么要让A和B去争用互斥锁呢。因为此时它两并不修改变量,所以同时并发读取是没有任何问题的,所以我们把锁切割为了两把,当读时,获取读锁,写时获取写锁,且读锁可以多个线程同时获取,此时我们称读锁为共享锁,写锁为互斥锁(只能一个线程获取)
那么我们知道,读写都有自己的状态,所以应该需要两个变量来表示读锁和写锁的状态
int readState;
int writeState;
int lockState;
这里为什么用整形值呢,因为读写锁都是可重入的,也就是同一个线程获取到锁之后,可以再次获取锁,需要记录
那么接下来简单实现下读锁和写锁的代码
// 写锁
lock lockState
if readState == 0
if CAS writeState 0 -> 1
return;
else currentThread == wirteThread // 当前获取锁的线程不是当前线程
writeState++
return;
// 读锁
lock lockState
if writeState == 0
if // 假如这里有很多个读线程,写线程很有可能会获取不到锁(线程饥饿) 解决:在获取读锁之前,判断当前有没有写锁排队即可
readState++
else
为什么要对lockState
变量上锁,因为两个if条件不是原子性操作,在进入第二个if前有可能被别的线程改了读或写锁的状态值,那这样写显得效率很慢,有没有什么既能表示读锁又能表示写锁呢,这时可以用一个32位的变量来表示,高16位表示读锁,低16位表示写锁,此时只需要操作一个变量,所以只需要一次CAS
即可,不需要多个操作完成的原子性
继续分析,我们知道读锁是共享锁,state变量高16位保存着所有线程读锁的可重入次数,那么我想知道某个线程读锁的重入次数,这该怎么办,可以通过ThreadLocal
来进行记录。
注意这里有两个优化:
第一个优化就是有没有一种可能就是只有一个线程获取锁呢,这时通过源码看到有个firstReaderHoldCount
变量来记录第一个线程可重入的次数,这样就不需要对ThreadLocal进行初始化,占用更多内存
第二个优化就是有没有一种可能就是在最后的时间段只有一个线程获取锁呢,也就是说最后一个线程可能会成为第一个线程,这时通过源码看到又个cachedHoldCounter
变量来记录最后可重入的次数,这样和上面同理,优化了内存
用例
将原来的锁,分割为两把锁:读锁、写锁。适用于读多写少的场景,读锁可以并发,写锁与其他锁互斥。写写互斥、写读互斥、读读兼容。
读和写公平锁和非公平锁
总结一句话就是看或看不看。
实现接口核心方法
public interface ReadWriteLock {
// 获取读锁
Lock readLock();
// 获取写锁
Lock writeLock();
}
readerLock
和writerLock
变量用于支撑以上描述的ReadWriteLock
接口的读锁和写锁方法。通过构造方法得知,读写锁对象的创建和用例均依赖于公平锁或者非公平锁同步器。
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable {
private static final long serialVersionUID = -6992448646407690164L;
// 读锁对象
private final ReentrantReadWriteLock.ReadLock readerLock;
// 写锁对象
private final ReentrantReadWriteLock.WriteLock writerLock;
// 同步器
final Sync sync;
public ReentrantReadWriteLock() {
// 默认非公平锁
this(false);
}
// 根据fair变量,来选择创建不同的锁:公平锁 FairSync 和非公平锁 NonfairSync
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
// 用同步器创造锁对象
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
Sync
类
核心变量和构造器
我们说读锁可以多个线程同时持有,而写锁只允许一个线程持有,此时我们称 读锁-----共享锁 写锁------互斥锁(排他锁)。然后我们在AQS
中了解到一个变量state
,它是32位的值,那么我们这里将其切割为高16位和低16位。
abstract static class Sync extends AbstractQueuedSynchronizer {
// 高16位用于表示读锁
static final int SHARED_SHIFT = 16;
// 用于对高16位操作:加1 减1
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
// 最大读锁量
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
// 用于获取低16位的值。例如 获取低八位:0000 0000 1111 1111
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
/** 获取当前持有读锁的线程数量 */
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
/** 获取当前持有写锁的线程数量 */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
// 高16位为所有读锁获取,那么我想知道每个线程对于读锁重入的次数?采用ThreadLocal来进行统计,每个线程自己统计自己的
static final class HoldCounter {
int count = 0;
final long tid = getThreadId(Thread.currentThread());
}
// 继承自ThreadLocal,重写了其中的initialValue方法,该方法将在线程第一次获取该变量时调用初始化HoldCounter计数器
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
// 创建ThreadLocal对象
private transient ThreadLocalHoldCounter readHolds;
// 缓存最后一个线程获取的读锁数量
private transient HoldCounter cachedHoldCounter;
// 保存获取到该锁的第一个读锁线程
private transient Thread firstReader = null;
// 保存第一个该锁的第一个读锁线程获取到的读锁数量
private transient int firstReaderHoldCount;
Sync() {
// 构造器中初始化ThreadLocalHoldCounter ThreadLocal对象
readHolds = new ThreadLocalHoldCounter();
// 用于保证可见性,使用了state变量的volatile语义
setState(getState());
}
}
tryAcquire()
获取写锁的流程
protected final boolean tryAcquire(int acquires) {
// 获取当前线程
Thread current = Thread.currentThread();
int c = getState();
// 获取写锁数量
int w = exclusiveCount(c);
// 状态值有效
if (c != 0) {
// 有线程获取到了读锁或者当前线程不是持有互斥锁的线程
if (w == 0 || current != getExclusiveOwnerThread())
// 返回false,让AQS执行线程阻塞放入到阻塞队列
return false;
// 写锁重入,而又由于写锁的数量保存在低16位,所以直接加就行了
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
setState(c + acquires);
return true;
}
// 既没有读锁也没有写锁
if (writerShouldBlock() || // 当前线程是否可以获取锁
!compareAndSetState(c, c + acquires)) // 通过CAS抢锁
// 获取锁失败,AQS执行阻塞操作
return false;
// 获取写锁成功,那么将当前线程标识为获取互斥锁的线程对象
setExclusiveOwnerThread(current);
return true;
}
tryAcquireShared()
获取读锁的流程
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 + SHARED_UNIT)) { // 直接CAS 增加state的高16位的读锁持有数量
// 增加高16位之前的计数为0,此时表明当前线程就是第一个获取读锁的线程
if (r == 0) {
// 注意:持有两个变量来优化threadlocal
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) { // 当前获取读锁的线程就是一个线程,那么此时表明:锁重入,直接++计数位即可
firstReaderHoldCount++;
} else {
// 当前线程不是第一个读线程,此时将其获取读锁的次数保存在ThreadLocal中
// 以下代码做了一次优化,就是很有可能最后一个线程会成为第一个线程,这时会记录最后一个线程读锁的数量
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;
}
// 在看doug lea写的代码时,请注意:经常做优化,就是把一些常见的场景前置,保证性能
return fullTryAcquireShared(current);
}
fullTryAcquireShared()
完全获取读锁流程
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) {
// 当前线程就是第一个获取到读锁的线程
} else {
// 获取到当前线程记录读锁重入次数的HoldCounter对象
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();
}
}
// 当前读锁重入次数为0时,表明没有获取读锁,此时返回-1,阻塞当前线程
if (rh.count == 0)
return -1;
}
}
// 读锁获取次数溢出
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// CAS增加读锁次数,剩下的流程和上面的一样
if (compareAndSetState(c, c + SHARED_UNIT)) {
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} 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;
}
return 1;
}
}
}
tryRelease()
释放写锁的流程
protected final boolean tryRelease(int releases) {
// 没有获取写锁,为啥能释放写锁呢?
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
// 释放完毕后,写锁状态是否为0(锁重入),因为此时计算的不是当前state,是nextc
boolean free = exclusiveCount(nextc) == 0;
// 如果下一个状态值为0,此时表明当前线程完全释放了锁,也即锁重入为0,那么将当前线程对象从OwnerThread中移除
if (free)
setExclusiveOwnerThread(null);
// 此时设置全局state变量即可
setState(nextc);
// 如果返回为true,那么由AQS完成后面线程的唤醒
return free;
}}
tryReleaseShared()
释放读锁的流程
释放时,需要考虑:重入多少次,就释放多少次。总结:先完成自己的释放,然后再完成共享的高16位的释放。
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
// 当前线程是第一个获取到读锁的线程
if (firstReader == current) {
// 当前重入次数为1,代表可以直接释放,如果不是1,那么表明还持有多个读锁,也即重入多次,那么直接--
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) {
// 当前线程已经释放完读锁,那么不需要在ThreadLocal里持有HoldCounter对象
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
for (;;) {
// CAS释放高16位计数
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// 释放完毕后是否为0,为无锁状态,此时需要干啥?由AQS来唤醒阻塞的线程
return nextc == 0;
}
}
readerShouldBlock
和writerShouldBlock
模板方法公平锁实现
判断条件只有一个:hasQueuedPredecessors()
方法,就是看看AQS的阻塞队列里是否有其他线程正在等待,如果有排队去。总结:有人在排队,那么不插队。w->r->r->r
此时来了个r:w->r->r->r->r
, 此时来了个w:w->r->r->r->w
。
static final class FairSync extends Sync {
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
} // w->r->r r获取锁 w->r->r-r
}
readerShouldBlock
和writerShouldBlock
模板方法非公平锁实现
写线程永远false,因为读写锁本身适用的是读多写少,此时不应该 让写线程饥饿,而且非公平,写锁永远不阻塞,让它抢,不管前面是否有人排队,先抢了再说。apparentlyFirstQueuedIsExclusive()第一个排队的是不是写线程。r(10)代表有10个线程持有读锁,当前线程是第十一个,此时已经有一个写线程排队,r(10)->w,此时排队去。r(10)->w->r。
static final class NonfairSync extends Sync {
final boolean writerShouldBlock() {
return false;
}
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
} // w->r->r r获取锁 r->r->r
}
Semaphore
介绍
作用:限制并发量
实现原理:通过设置permits数量,每获取一个permit,就减1,直到减为0为止
得知:使用AQS作为模板类,然后使用其共享锁机制,实现了公平锁和非公平锁来完成Semaphore信号量语义。获取permit的acquire(int permits)操作、释放permit的release(int permits)操作,均是由sync类来完成的
public class Semaphore implements java.io.Serializable {
private final Sync sync;
static final class FairSync extends Sync {}
static final class NonfairSync extends Sync {}
abstract static class Sync extends AbstractQueuedSynchronizer {}
public void acquire(int permits) throws InterruptedException {
if (permits < 0) throw new IllegalArgumentException();
sync.acquireSharedInterruptibly(permits);
}
public void release(int permits) {
if (permits < 0) throw new IllegalArgumentException();
sync.releaseShared(permits);
}
}
acquireSharedInterruptibly方法原理
该代码由AQS完成,我们不做过多赘述,主要看这里的Interruptibly的意思,此时相当于响应线程的中断。最后还是调用子类的tryAcquireShared模板方法,让子类实现自己获取信号量的机制。
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
我们先来看公平锁的实现:
state变量用于标识permit值。
static final class FairSync extends Sync {
// 构造器用于初始化父类AQS的state变量
FairSync(int permits) {
super(permits);
}
protected int tryAcquireShared(int acquires) {
for (;;) {
// 如果有线程在AQS的队列中排队,那么返回-1,将由AQS完成阻塞操作
if (hasQueuedPredecessors())
return -1;
int available = getState();
int remaining = available - acquires;
// 如果此时有可用的多余的信号量,那么进行CAS操作,如果失败,那么返回剩下的资源数。如果此时CAS成功,那么返回的资源数就为当前值,有可能为0或者大于0
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
}
非公平锁的实现:
static final class NonfairSync extends Sync {
NonfairSync(int permits) {
super(permits);
}
// 由父类Sync来完成调用
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
}
// sync类实现方法
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
// 非公平锁直接CAS抢即可,直到可用资源数小于0
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
releaseShared方法原理
AQS实现方法,我们可以看到当模板方法tryReleaseShared,由子类完成释放后,那么将会调用doReleaseShared方法唤醒后面等待的线程。
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
直接看Sync类的实现:
protected final boolean tryReleaseShared(int releases) {
for (;;) {
// 直接通过CAS操作对state变量+1即可
int current = getState();
int next = current + releases;
if (next < current)
throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next))
return true;
}
}
CountDownLautch
介绍
定义
是一个同步器,用于一个或者多个线程等待其他线程完成一组操作,原理如下:
1、AQS
的state
变量用于表示操作个数
2、AQS
的共享锁机制完成唤醒
3、等待锁的线程使用acquireShared
方法获取共享锁等待
4、操作线程使用releaseShared
方法用于唤醒等待共享锁的线程
构造器原理
从构造器中得出,count
值和核心操作均由内部同步器类Sync
完成
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
countDown
方法原理
public void countDown() {
sync.releaseShared(1);
}
releaseShared
方法
该方法由AQS
来实现,可以看到通过子类完成tryReleaseShared
方法释放共享锁,如果释放成功,那么直接调用doReleaseShared
方法完成等待获取共享锁的线程,获取共享锁。
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer{
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
}
tryReleaseShared
方法实现过程
protected boolean tryReleaseShared(int releases) {
for (;;) {
// 获取当前state值,代表了完成的操作个数
int c = getState();
if (c == 0)
return false;
// 计算更新值,CAS原子性的修改即可
int nextc = c-1;
if (compareAndSetState(c, nextc))
// 若修改成功,那么判断当前线程是不是最后一个完成操作的线程,如果是,那么返回true,此时唤醒所有等待共享锁的线程
return nextc == 0;
}
}
await
方法原理
public boolean await(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
tryAcquireSharedNanos
方法原理
该方法由AQS
实现,可以看到方法中抛出了InterruptedException
中断异常,由此可见该方法响应了线程中断,但是核心操作还是由子类来实现tryAcquireShared(arg)
来完成共享锁的获取操作。
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer{
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquireShared(arg) >= 0 ||
doAcquireSharedNanos(arg, nanosTimeout); // 该方法在后面说AQS时完成讲解
}
}
tryAcquireShared
方法实现
就是看变量是否为0,如果为0,那么无条件返回1,此时将会直接获取到共享锁。
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}