if (delay <= 0) //小于等于0,时间到了
return q.poll();
first = null; // don’t retain ref while waiting
if (leader != null)
available.await();//没有抢到leader的线程进入等待,避免大量唤醒操作
else {
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
available.awaitNanos(delay);//leader线程,在等待一定时间后再次尝试获取
} finally {
if (leader == thisThread)//重置leader
leader = null;
}
}
}
}
} finally {
if (leader == null && q.peek() != null)
available.signal();
lock.unlock();
}
}
//…
}
//继承了Comparable
public interface Delayed extends Comparable {
long getDelay(TimeUnit unit);
}
小结:
延迟队列就是个容器,需要调用方获取任务和执行任务。整体实现比较简单,利用优先队列实现的,元素通过实现 Delayed 接口来返回延迟的时间。
▐ 总结
三者实现思想基本类似,都围绕三个要素:任务、任务的组织者(队列),执行者调度执行者。
Timer、ScheduledThreadPoolExecutor 完整的实现了这三个要素,DelayQueue只实现了任务组织者这个要素,需要与线程配合使用。其中任务组织者这个要素,它们都是通过优先队列来实现,因此插入和删除任务的时间复杂度都为O(logn),并且 Timer 、ScheduledThreadPool 的周期性任务是通过重置任务的下一次执行时间来完成的。
个人觉得在时间复杂度上存在较大的性能风险,插入删除时间复杂度是O(logn),如果面对大数量级的任务频繁的插入、删除,这种实现方式性能问题会比较严重。
**时间轮算法
**
前面学习中基本可以总结出延迟任务有这几种调度:
-
约定一段时间后执行
-
约定某个时间点执行
-
周期性执行。
1、2,这两者之间可以相互转换,比如给你个任务,要求12点钟执行,你看了一眼时间,发现现在是9点钟,那么你可以认为这个任务过三个小时执行。同样的,给你个任务让你3个小时后执行,你看了一眼现在是9点钟,那么你当然可以认为这个任务12点钟执行。
对于3,需重复执行的任务,其实我们需要关心的只是下次执行时间,并不关心这个任务需要循环多少次,比如每天上午9点执行生成报表的任务,在当天9点任务执行时发现是周期性任务,只需要将该任务下次执行时间设置成明天9点即可,以此重复,直至到了结束时间,将任务移除。我们可以将这些场景抽象用时间轮管理,时间轮就是和钟表很相似的存在!
时间轮用环形数组实现,数组的每个元素可以称为槽,槽的内部用双向链表存着待执行的任务,添加和删除的链表操作时间复杂度都是 O(1),槽位本身也指代时间精度,比如一秒扫一个槽,那么这个时间轮的最高精度就是 1 秒。也就是说延迟 1.2 秒的任务和 1.5 秒的任务会被加入到同一个槽中,然后在 1 秒的时候遍历这个槽中的链表执行任务。
案例如图,从图中可以看到此时指针指向的是第2个槽(下标1),一共有八个槽0~7,假设槽的时间单位为 1 秒,现在要加入一个延时 4 秒的任务,计算方式就是 4 % 8 + 2 = 6,即放在槽位为 6,下标为 5 的那个槽中,就是拼到槽的双向链表的尾部。然后每秒指针顺时针移动一格,这样就扫到了下一格,遍历这格中的双向链表执行任务。然后再循环继续。可以看到插入任务从计算槽位到插入链表,时间复杂度都是O(1)。
那假设现在要加入一个50秒后执行的任务怎么办?这槽好像不够啊?难道要加槽嘛?和HashMap一样扩容?
常见有两种方式:
**一种是通过增加轮次的概念,**先计算槽位:50 % 8 + 2 = 4,即应该放在槽位是 4,下标是 3 的位置。然后计算轮次:(50 - 1) / 8 = 6,即轮数记为 6。也就是说当循环 6 轮之后扫到下标的 3 的这个槽位会触发这个任务。Netty 中的 HashedWheelTimer 使用的就是这种方式。
**
还有一种是通过多层次的时间轮,**这个和我们的手表就更像了,像我们秒针走一圈,分针走一格,分针走一圈,时针走一格,多层次时间轮就是这样实现的。假设上图就是第一层,那么第一层走了一圈,第二层就走一格,可以得知第二层的一格就是8秒,假设第二层也是 8 个槽,那么第二层走一圈,第三层走一格,可以得知第三层一格就是 64 秒。那么一个三层,每层8个槽,一共24个槽时间轮就可以处理最多延迟 512 秒的任务。
而多层次时间轮还会有降级的操作,假设一个任务延迟500秒执行,那么刚开始加进来肯定是放在第三层的,当时间过了 436 秒后,此时还需要 64 秒就会触发任务的执行,而此时相对而言它就是个延迟64秒后的任务,因此它会被降低放在第二层中,第一层还放不下它。再过个 56 秒,相对而言它就是个延迟8秒后执行的任务,因此它会再被降级放在第一层中,等待执行。
降级是为了保证时间精度一致性,Kafka内部用的就是多层次的时间轮算法。
时间轮算法应用案例
很有需要延迟操作的应用场景中都有时间轮的身影,比如Netty、Kafka、Akka、Zookeeper等组件,我们挑选上面聊到的时间轮的两种实现方式的典型代表学习一下,轮次时间轮算法实现-Netty,多层次时间轮算法实现-Kafka。
▐ 轮次时间轮算法-Netty案例
在 Netty 中时间轮的实现类是 HashedWheelTimer,代码中的wheel就是时间轮循环数组,mask就是取余找槽位的逻辑,不过这里的设计通过限制槽位数组的大小为2的次方,然后利用位运算来替代取模运算,提高性能。
tickDuration就是每格的时间精度,可以看到配备了一个工作线程来处理任务的执行。以及双向链路的槽HashedWheelBucket。
public class HashedWheelTimer implements Timer {
//…
public HashedWheelTimer(ThreadFactory threadFactory, long tickDuration, TimeUnit unit) {
this(threadFactory, tickDuration, unit, 512);
}
public HashedWheelTimer(ThreadFactory threadFactory, long tickDuration, TimeUnit unit, int ticksPerWheel, boolean leakDetection, long maxPendingTimeouts) {
this.worker = new HashedWheelTimer.Worker();
this.workerState = 0;
this.startTimeInitialized = new CountDownLatch(1);
this.timeouts = PlatformDependent.newMpscQueue();
this.cancelledTimeouts = PlatformDependent.newMpscQueue();
this.pendingTimeouts = new AtomicLong(0L);
if (threadFactory == null) {
throw new NullPointerException(“threadFactory”);
} else if (unit == null) {
throw new NullPointerException(“unit”);
} else if (tickDuration <= 0L) {
throw new IllegalArgumentException("tickDuration must be greater than 0: " + tickDuration);
} else if (ticksPerWheel <= 0) {
throw new IllegalArgumentException("ticksPerWheel must be greater than 0: " + ticksPerWheel);
} else {
this.wheel = createWheel(ticksPerWheel);//默认512的槽数量
this.mask = this.wheel.length - 1;//mask设计提高了“确定槽位下标”的性能,通过限制wheel.length为2的N次方,
this.tickDuration = unit.toNanos(tickDuration);//时间精度,即每个槽位的时间跨度
if (this.tickDuration >= 9223372036854775807L / (long)this.wheel.length) {
throw new IllegalArgumentException(String.format(“tickDuration: %d (expected: 0 < tickDuration in nanos < %d”, tickDuration, 9223372036854775807L / (long)this.wheel.length));
} else {
this.workerThread = threadFactory.newThread(this.worker);//工作线程
this.leak = !leakDetection && this.workerThread.isDaemon() ? null : leakDetector.track(this);
this.maxPendingTimeouts = maxPendingTimeouts;
if (INSTANCE_COUNTER.incrementAndGet() > 64 && WARNED_TOO_MANY_INSTANCES.compareAndSet(false, true)) {
reportTooManyInstances();
}
}
}
}
//…
//双向链表的bucket
private static final class HashedWheelBucket {
private HashedWheelTimer.HashedWheelTimeout head;
private HashedWheelTimer.HashedWheelTimeout tail;
//…
}
//链表元素,任务
private static final class HashedWheelTimeout implements Timeout {
HashedWheelTimer.HashedWheelTimeout next;
HashedWheelTimer.HashedWheelTimeout prev;
HashedWheelTimer.HashedWheelBucket bucket;
//…
}
}
我们从任务的添加作为切入点,看下整体的逻辑。
public Timeout newTimeout(TimerTask task, long delay, TimeUnit unit) {
if (task == null) {
throw new NullPointerException(“task”);
} else if (unit == null) {
throw new NullPointerException(“unit”);
} else {
long pendingTimeoutsCount = this.pendingTimeouts.incrementAndGet();
if (this.maxPendingTimeouts > 0L && pendingTimeoutsCount > this.maxPendingTimeouts) {
this.pendingTimeouts.decrementAndGet();
throw new RejectedExecutionException(“Number of pending timeouts (” + pendingTimeoutsCount + “) is greater than or equal to maximum allowed pending timeouts (” + this.maxPendingTimeouts + “)”);
} else {
this.start();//启动工作线程,内部有判断状态
long deadline = System.nanoTime() + unit.toNanos(delay) - this.startTime;//计算延迟时间
HashedWheelTimer.HashedWheelTimeout timeout = new HashedWheelTimer.HashedWheelTimeout(this, task, deadline);//创建任务对象
this.timeouts.add(timeout);//添加到队列中,这边标记问题,没看到添加到时间轮中?
return timeout;
}
}
}
我们可以看到,整体逻辑比较清晰:启动工作线程-》创建任务对象-》放入到队列中。此处有个疑问,任务没有直接添加到时间轮中,而是被添加到队列中?唯一的线索是启动了工作线程,我们看一下工作线程的逻辑。
private final class Worker implements Runnable {//工作线程
//…
public void run() {
HashedWheelTimer.this.startTime = System.nanoTime();//启动时间
if (HashedWheelTimer.this.startTime == 0L) {
HashedWheelTimer.this.startTime = 1L;
}
HashedWheelTimer.this.startTimeInitialized.countDown();
int idx;
HashedWheelTimer.HashedWheelBucket bucket;
do {
long deadline = this.waitForNextTick();//等待执行任务时间到来
if (deadline > 0L) {
idx = (int)(this.tick & (long)HashedWheelTimer.this.mask);//或者槽位下标
this.processCancelledTasks();//先处理取消了的任务
bucket = HashedWheelTimer.this.wheel[idx];//获取对应的槽
this.transferTimeoutsToBuckets();//将添加到任务队列中的任务添加到时间轮中的槽中
bucket.expireTimeouts(deadline);//处理时间到了的任务
++this.tick;//移动槽位
}
} while(HashedWheelTimer.WORKER_STATE_UPDATER.get(HashedWheelTimer.this) == 1);
//…
}
通过代码查阅,整体逻辑简单清晰:等待时间-》处理取消的任务-》队列中的任务入槽-》处理执行的任务。
主要看下这三个问题:
-
等待时间是如何计算的,这个跟时间精度相关
-
队列中的任务如何入槽的(对应上面的疑问)
-
任务如何执行的
等待时间是如何计算的?worker.waitForNextTick
private long waitForNextTick() {
long deadline = HashedWheelTimer.this.tickDuration * (this.tick + 1L);//根据时间精度,算出需要下一次的检查时间
while(true) {
long currentTime = System.nanoTime() - HashedWheelTimer.this.startTime;
long sleepTimeMs = (deadline - currentTime + 999999L) / 1000000L;
if (sleepTimeMs <= 0L) {//不用睡了,时间已经到了,直接执行
if (currentTime == -9223372036854775808L) {//溢出了兜底?
return -9223372036854775807L;
}
return currentTime;
}
if (PlatformDependent.isWindows()) {//windows下的bug,sleep的时间需是10的整数倍
sleepTimeMs = sleepTimeMs / 10L * 10L;
}
try {
Thread.sleep(sleepTimeMs);//等待时间的到来
} catch (InterruptedException var8) {
if (HashedWheelTimer.WORKER_STATE_UPDATER.get(HashedWheelTimer.this) == 2) {
return -9223372036854775808L;
}
}
}
}
就是通过tickDuration和此时已经移动的tick算出下一次需要检查的时间,如果时间未到就sleep。
队列中的任务如何入槽的?worker.transferTimeoutsToBuckets
private void transferTimeoutsToBuckets() {
for(int i = 0; i < 100000; ++i) {//设置了一次性处理10w个任务入槽,可能考虑这块处理太多会影响后续任务的处理?
HashedWheelTimer.HashedWheelTimeout timeout = (HashedWheelTimer.HashedWheelTimeout)HashedWheelTimer.this.timeouts.poll();//从队列中拿出任务
if (timeout == null) {
break;
}
if (timeout.state() != 1) {
long calculated = timeout.deadline / HashedWheelTimer.this.tickDuration;
timeout.remainingRounds = (calculated - this.tick) / (long)HashedWheelTimer.this.wheel.length;//计算轮数
long ticks = Math.max(calculated, this.tick);//如果时间已经过了,放到当前即将执行的槽位中
int stopIndex = (int)(ticks & (long)HashedWheelTimer.this.mask);//计算槽位下标
HashedWheelTimer.HashedWheelBucket bucket = HashedWheelTimer.this.wheel[stopIndex];
bucket.addTimeout(timeout);//入槽
}
}
}
任务如何执行的?hashedWheelBucket.expireTimeouts
public void expireTimeouts(long deadline) {
HashedWheelTimer.HashedWheelTimeout next;
for(HashedWheelTimer.HashedWheelTimeout timeout = this.head; timeout != null; timeout = next) {
//循环链表
next = timeout.next;
if (timeout.remainingRounds <= 0L) {//处理轮数为0的任务
next = this.remove(timeout);
if (timeout.deadline > deadline) {
throw new IllegalStateException(String.format(“timeout.deadline (%d) > deadline (%d)”, timeout.deadline, deadline));
}
timeout.expire();//过期任务,就是执行任务的run防范
} else if (timeout.isCancelled()) {
next = this.remove(timeout);//任务取消了
} else {
–timeout.remainingRounds;//轮数减一
}
}
}
就是通过轮数和时间双重判断,执行任务。
小结:
Netty中时间轮算法是基于轮次的时间轮算法实现,通过启动一个工作线程,根据时间精度TickDuration,移动指针找到槽位,根据轮次+时间来判断是否是需要处理的任务。
不足之处:
-
时间轮的推进是根据时间精度TickDuration来固定推进的,如果槽位中无任务,也需要移动指针,会造成无效的时间轮推进,比如TickDuration为1秒,此时就一个延迟500秒的任务,那就是有499次无用的推进。
-
任务的执行都是同一个工作线程处理的,并且工作线程的除了处理执行到时的任务还做了其他操作,因此任务不一定会被精准的执行,而且任务的执行如果不是新起一个线程执行,那么耗时的任务会阻塞下个任务的执行。
优势就是时间精度可控,并且增删任务的时间复杂度都是O(1)
▐ 多层次时间轮算法-Kafka案例
去kafka官网下载源码(http://kafka.apache.org/downloads ),在kafka.utils.timer中找到基于scala实现的时间轮。
主要有Timer接口–SystemTimer实现了该接口,是核心的调度逻辑;TimingWheel–时间轮数据结构的实现;TimerTaskList–槽位中的链表;TimerTaskEntry–链表元素,TimerTask的包装类,在TimerTaskList中定义;TimerTask–继承了Runnable,实际的任务执行线程。
老规矩,我们从添加任务、任务调度(工作线程)两个切入点看一下整体逻辑,添加任务逻辑在TimingWheel中,工作线程在SystemTimer中。
TimingWheel–时间轮数据结构的核心实现
@nonthreadsafe
private[timer] class TimingWheel(tickMs: Long, wheelSize: Int, startMs: Long, taskCounter: AtomicInteger, queue: DelayQueue[TimerTaskList]) {
//queue是一个TimerTaskList的延迟队列,每个槽的TimerTaskList都被加到这个延迟队列中,expireTime最小的槽会排在队列的最前面,此处要注意,这是kafka实现的一个特殊地方
private[this] val interval = tickMs * wheelSize //该层级时间轮的时间跨度
private[this] val buckets = Array.tabulateTimerTaskList { _ => new TimerTaskList(taskCounter) }
private[this] var currentTime = startMs - (startMs % tickMs) //起始时间(startMs)都设置为创建此层时间轮时前面一轮的currentTime,每一层的currentTime都必须是tickMs的整数倍
@volatile private[this] var overflowWheel: TimingWheel = null
//当时间溢出时,需要新增上一级时间轮,通过overflowWheel引用上一级侧时间轮
private[this] def addOverflowWheel(): Unit = {
synchronized {
if (overflowWheel == null) {
overflowWheel = new TimingWheel(
tickMs = interval,
wheelSize = wheelSize,
startMs = currentTime,
taskCounter = taskCounter,
queue
)
}
}
}
//添加任务
def add(timerTaskEntry: TimerTaskEntry): Boolean = {
val expiration = timerTaskEntry.expirationMs//获取任务延迟时间
if (timerTaskEntry.cancelled) {
false//取消
} else if (expiration < currentTime + tickMs) {
false // 过期
} else if (expiration < currentTime + interval) {
//判断当前时间轮所能表示的时间范围是否可以容纳该任务
//计算槽位
val virtualId = expiration / tickMs
val bucket = buckets((virtualId % wheelSize.toLong).toInt)
bucket.add(timerTaskEntry)//将任务添加到槽位链表中
if (bucket.setExpiration(virtualId * tickMs)) {//设置expiretime
//将槽位的TimerTaskList添加到延迟队列中
queue.offer(bucket)
}
true
} else {
//到此处,说明时间溢出了,应该讲任务丢给父时间轮处理
if (overflowWheel == null) addOverflowWheel()//判断上一级时间轮是否存在,不存在创建
overflowWheel.add(timerTaskEntry)//调用上一级时间轮的添加任务方法,递归处理,直至添加成功
}
}
//时间轮的推进
def advanceClock(timeMs: Long): Unit = {
if (timeMs >= currentTime + tickMs) {
currentTime = timeMs - (timeMs % tickMs)
//调整当前时间
if (overflowWheel != null) overflowWheel.advanceClock(currentTime)//递归推进上级时间轮
}
}
这是kafka对于时间轮最核心的实现部分,包含时间轮的数据结构、添加任务、时间溢出(添加上一级时间轮)、时间轮推进四个核心部分。大的逻辑是添加任务-》是否时间溢出?-》溢出时添加上一级时间轮,并调用上一级时间轮的添加任务方法 -》未溢出,直接添加到槽位 -》递归处理。所以时间轮的数据结构、时间溢出都通过添加任务的逻辑串联了起来。而时间轮推进方法主要由工作线程SystemTimer调用。
SystemTimer–核心的调度逻辑
trait Timer {
def add(timerTask: TimerTask): Unit
def advanceClock(timeoutMs: Long): Boolean
def size: Int
def shutdown(): Unit
}
@threadsafe
class SystemTimer(executorName: String,
tickMs: Long = 1,
wheelSize: Int = 20,
startMs: Long = Time.SYSTEM.hiResClockMs) extends Timer {
// timeout timer
private[this] val taskExecutor = Executors.newFixedThreadPool(1,
(runnable: Runnable) => KafkaThread.nonDaemon(“executor-” + executorName, runnable))
private[this] val delayQueue = new DelayQueueTimerTaskList//延迟队列,会传递到每个层级的时间轮中
private[this] val taskCounter = new AtomicInteger(0)
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
最后
按照上面的过程,4个月的时间刚刚好。当然Java的体系是很庞大的,还有很多更高级的技能需要掌握,但不要着急,这些完全可以放到以后工作中边用别学。
学习编程就是一个由混沌到有序的过程,所以你在学习过程中,如果一时碰到理解不了的知识点,大可不必沮丧,更不要气馁,这都是正常的不能再正常的事情了,不过是“人同此心,心同此理”的暂时而已。
“道路是曲折的,前途是光明的!”
一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
1712807548958)]
[外链图片转存中…(img-39Vi4lbu-1712807548959)]
[外链图片转存中…(img-dxnc6BzB-1712807548959)]
[外链图片转存中…(img-LbDc9khS-1712807548959)]
[外链图片转存中…(img-RgUKRPGs-1712807548960)]
[外链图片转存中…(img-oAVXOECY-1712807548960)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-BwsQDoVp-1712807548961)]
最后
按照上面的过程,4个月的时间刚刚好。当然Java的体系是很庞大的,还有很多更高级的技能需要掌握,但不要着急,这些完全可以放到以后工作中边用别学。
学习编程就是一个由混沌到有序的过程,所以你在学习过程中,如果一时碰到理解不了的知识点,大可不必沮丧,更不要气馁,这都是正常的不能再正常的事情了,不过是“人同此心,心同此理”的暂时而已。
“道路是曲折的,前途是光明的!”
[外链图片转存中…(img-IUVp6sN3-1712807548961)]
[外链图片转存中…(img-wGoRfPC2-1712807548961)]
一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
[外链图片转存中…(img-S58QlpR7-1712807548962)]