1、基本架构
DelayedWorkQueue
的实现原理中规中矩,内部维护了一个以RunnableScheduledFuture
类型数组实现的最小二叉堆,初始容量是16,使用ReentrantLock
和Condition
实现生产者和消费者模式。
static class DelayedWorkQueue extends AbstractQueue
implements BlockingQueue {
private static final int INITIAL_CAPACITY = 16;
private RunnableScheduledFuture<?>[] queue =
new RunnableScheduledFuture<?>[INITIAL_CAPACITY];
private final ReentrantLock lock = new ReentrantLock();
private int size = 0;
private Thread leader = null;
private final Condition available = lock.newCondition();
}
2、offer添加元素
ScheduledThreadPoolExecutor
提交任务时调用的是DelayedWorkQueue.add
,而add
、put
等一些对外提供的添加元素的方法都调用了offer
,其基本流程如下:
-
其作为生产者的入口,首先获取锁。
-
判断队列是否要满了(
size >= queue.length
),满了就扩容grow()
。 -
队列未满,size+1。
-
判断添加的元素是否是第一个,是则不需要堆化。
-
添加的元素不是第一个,则需要堆化
siftUp
。 -
如果堆顶元素刚好是此时被添加的元素,则唤醒take线程消费。
-
最终释放锁。
public boolean offer(Runnable x) {
if (x == null)
throw new NullPointerException();
RunnableScheduledFuture<?> e = (RunnableScheduledFuture<?>)x;
final ReentrantLock lock = this.lock;
lock.lock();
try {
int i = size;
if (i >= queue.length)
//扩容
grow();
size = i + 1;
if (i == 0) {
//如果是入的是第一个元素,不需要堆化
queue[0] = e;
setIndex(e, 0);
} else {
//堆化
siftUp(i, e);
}
if (queue[0] == e) {
//如果堆顶元素刚好是入队列的元素,则唤醒take
leader = null;
available.signal();
}
} finally {
lock.unlock();
}
return true;
}
如图为offer
基本流程图:
(1)扩容grow
可以看到,当队列满时,不会阻塞等待,而是继续扩容。新容量newCapacity
在旧容量oldCapacity
的基础上扩容50%(oldCapacity >> 1
相当于oldCapacity /2
)。最后Arrays.copyOf
,先根据newCapacity
创建一个新的空数组,然后将旧数组的数据复制到新数组中。
private void grow() {
int oldCapacity = queue.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // grow 50%
if (newCapacity < 0) // overflow
newCapacity = Integer.MAX_VALUE;
queue = Arrays.copyOf(queue, newCapacity);
}
(2)向上堆化siftUp
新添加的元素先会加到堆底,然后一步步和上面的父亲节点比较,若小于父亲节点则和父亲节点互换位置,循环比较直至大于父亲节点才结束循环。
private void siftUp(int k, RunnableScheduledFuture<?> key) {
while (k > 0) {
//找到父亲节点
int parent = (k - 1) >>> 1;
RunnableScheduledFuture<?> e = queue[parent];
if (key.compareTo(e) >= 0)
// 添加的元素 大于父亲节点,则结束循环
break;
//添加的元素小于父亲节点,则位置互换
queue[k] = e;
setIndex(e, k);
k = parent;
}
queue[k] = key;
setIndex(key, k);
}
如下图为siftUp
向上堆化过程图:
3、take消费元素
Worker
工作线程启动后就会循环消费工作队列中的元素,因为ScheduledThreadPoolExecutor
的keepAliveTime=0
,所以消费任务其只调用了DelayedWorkQueue.take
。take基本流程如下:
-
首先获取可中断锁,判断堆顶元素是否是空,空的则阻塞等待
available.await()
。 -
堆顶元素不为空,则获取其延迟执行时间
delay
,delay <= 0
说明到了执行时间,出队列finishPoll
。 -
delay > 0
还没到执行时间,判断leader
线程是否为空,不为空则说明有其他take线程也在等待,当前take将无限期阻塞等待。 -
leader
线程为空,当前take线程设置为leader
,并阻塞等待delay
时长。 -
当前leader线程等待delay时长自动唤醒护着被其他take线程唤醒,则最终将
leader
设置为null
。 -
再循环一次判断
delay <= 0
出队列。 -
跳出循环后判断leader为空并且堆顶元素不为空,则唤醒其他take线程,最后是否锁。
public RunnableScheduledFuture<?> take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;😉 {
RunnableScheduledFuture<?> first = queue[0]; //取出堆顶元素
if (first == null)
//堆为空,等待
available.await();
else {
long delay = first.getDelay(NANOSECONDS);
if (delay <= 0)
//到了执行时间,出队列
return finishPoll(first);
first = null; // don’t retain ref while waiting
//还没到执行时间
if (leader != null)
//此时若有其他take线程在等待,当前take将无限期等待
available.await();
else {
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
available.awaitNanos(delay);
} finally {
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
if (leader == null && queue[0] != null)
available.signal();
lock.unlock();
}
}
如下图为take基本流程图:
(1)take线程阻塞等待
可以看出这个生产者take线程会在两种情况下阻塞等待:
-
堆顶元素为空。
-
堆顶元素的delay>0。
(2)finishPoll出队列
堆顶元素delay<=0
,执行时间到,出队列就是一个向下堆化的过程siftDown
。
private RunnableScheduledFuture<?> finishPoll(RunnableScheduledFuture<?> f) {
int s = --size;
RunnableScheduledFuture<?> x = queue[s];
queue[s] = null;
if (s != 0)
siftDown(0, x);
setIndex(f, -1);
return f;
}
(3)siftDown向下堆化
由于堆顶元素出队列后,就破坏了堆的结构,需要组织整理下,将堆尾元素移到堆顶,然后向下堆化:
-
从堆顶开始,父亲节点与左右子节点中较小的孩子节点比较(左孩子不一定小于右孩子)。
-
父亲节点小于等于较小孩子节点,则结束循环,不需要交换位置。
-
若父亲节点大于较小孩子节点,则交换位置。
-
继续向下循环判断父亲节点和孩子节点的关系,直到父亲节点小于等于较小孩子节点才结束循环。
private void siftDown(int k, RunnableScheduledFuture<?> key) {
//k = 0, key = queue[size-1]
//无符号右移,相当于size/2
int half = size >>> 1;
while (k < half) {
//只需要比较一半
//找到左孩子节点
// child = 2k + 1
int child = (k << 1) + 1;
RunnableScheduledFuture<?> c = queue[child];
//右孩子节点
int right = child + 1;
//比较左右孩子大小
if (right < size && c.compareTo(queue[right]) > 0)
//c左孩子大于右孩子,则将有孩子赋值给左孩子
c = queue[child = right];
//比较key和孩子c
if (key.compareTo© <= 0)
//key小于等于c,则结束循环
break;
//key大于孩子c,则key与孩子交换位置
queue[k] = c;
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
![img](https://img-blog.csdnimg.cn/img_convert/6ca631b26379e852888b3e727c9aa70c.jpeg)
总结
机会是留给有准备的人,大家在求职之前应该要明确自己的态度,熟悉求职流程,做好充分的准备,把一些可预见的事情做好。
对于应届毕业生来说,校招更适合你们,因为绝大部分都不会有工作经验,企业也不会有工作经验的需求。同时,你也不需要伪造高大上的实战经验,以此让自己的简历能够脱颖而出,反倒会让面试官有所怀疑。
你在大学时期应该明确自己的发展方向,如果你在大一就确定你以后想成为Java工程师,那就不要花太多的时间去学习其他的技术语言,高数之类的,不如好好想着如何夯实Java基础。下图涵盖了应届生乃至转行过来的小白要学习的Java内容:
请转发本文支持一下
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
求职之前应该要明确自己的态度,熟悉求职流程,做好充分的准备,把一些可预见的事情做好。
对于应届毕业生来说,校招更适合你们,因为绝大部分都不会有工作经验,企业也不会有工作经验的需求。同时,你也不需要伪造高大上的实战经验,以此让自己的简历能够脱颖而出,反倒会让面试官有所怀疑。
你在大学时期应该明确自己的发展方向,如果你在大一就确定你以后想成为Java工程师,那就不要花太多的时间去学习其他的技术语言,高数之类的,不如好好想着如何夯实Java基础。下图涵盖了应届生乃至转行过来的小白要学习的Java内容:
请转发本文支持一下
[外链图片转存中…(img-fGa0vM6s-1713381595282)]
[外链图片转存中…(img-0KstFgWr-1713381595282)]
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!