ScheduledThreadPoolExecutor源码解读(一)——DelayedWorkQueue高度定制延迟阻塞优先工作队列


1、基本架构

DelayedWorkQueue的实现原理中规中矩,内部维护了一个以RunnableScheduledFuture类型数组实现的最小二叉堆,初始容量是16,使用ReentrantLockCondition实现生产者和消费者模式。

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,而addput等一些对外提供的添加元素的方法都调用了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基本流程图:

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向上堆化过程图:

siftup

3、take消费元素

Worker工作线程启动后就会循环消费工作队列中的元素,因为ScheduledThreadPoolExecutorkeepAliveTime=0,所以消费任务其只调用了DelayedWorkQueue.take。take基本流程如下:

  • 首先获取可中断锁,判断堆顶元素是否是空,空的则阻塞等待available.await()

  • 堆顶元素不为空,则获取其延迟执行时间delaydelay <= 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基本流程图:

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开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

总结

机会是留给有准备的人,大家在求职之前应该要明确自己的态度,熟悉求职流程,做好充分的准备,把一些可预见的事情做好。

对于应届毕业生来说,校招更适合你们,因为绝大部分都不会有工作经验,企业也不会有工作经验的需求。同时,你也不需要伪造高大上的实战经验,以此让自己的简历能够脱颖而出,反倒会让面试官有所怀疑。

你在大学时期应该明确自己的发展方向,如果你在大一就确定你以后想成为Java工程师,那就不要花太多的时间去学习其他的技术语言,高数之类的,不如好好想着如何夯实Java基础。下图涵盖了应届生乃至转行过来的小白要学习的Java内容:

请转发本文支持一下

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
求职之前应该要明确自己的态度,熟悉求职流程,做好充分的准备,把一些可预见的事情做好。

对于应届毕业生来说,校招更适合你们,因为绝大部分都不会有工作经验,企业也不会有工作经验的需求。同时,你也不需要伪造高大上的实战经验,以此让自己的简历能够脱颖而出,反倒会让面试官有所怀疑。

你在大学时期应该明确自己的发展方向,如果你在大一就确定你以后想成为Java工程师,那就不要花太多的时间去学习其他的技术语言,高数之类的,不如好好想着如何夯实Java基础。下图涵盖了应届生乃至转行过来的小白要学习的Java内容:

请转发本文支持一下

[外链图片转存中…(img-fGa0vM6s-1713381595282)]

[外链图片转存中…(img-0KstFgWr-1713381595282)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 24
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
《Linux0.11内核源码解读第一季——汇编启动部分》是一本深入解析Linux0.11内核源代码的书籍。汇编启动部分是整个内核启动过程中的重要环节,能够帮助读者深入理解操作系统的启动和初始化流程。 首先,汇编启动部分是内核启动的第一步。它通过软件中断机制在实模式下启动。通过设置系统段描述符和全局描述符表,为操作系统提供必要的运行环境。在启动过程中,汇编启动部分会初始化中断向量表、设置栈段(SS)和栈指针(SP),并跳转到引导扇区加载内核文件。 接着,书籍详细分析了引导扇区的装载过程。引导扇区会被BIOS加载到内存地址0x7C00处,然后执行引导扇区的代码。在引导扇区中,汇编启动部分会进行一些必要的初始化工作,如设置栈段和栈指针,加载中断描述符表以及读取磁盘上的内核文件。 此外,书籍还介绍了一些启动相关的概念和知识,如分段机制、实模式和保护模式之间的切换等。读者通过学习这些知识,可以更加清楚地了解硬件和操作系统之间的交互过程。 总之,汇编启动部分是Linux0.11内核启动的关键环节,对于理解操作系统的启动过程非常重要。《Linux0.11内核源码解读第一季——汇编启动部分》通过深入剖析源代码,让读者能够全面了解Linux内核的启动过程,并通过这些知识来探索更深入的操作系统原理。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值