任务调度:时间轮算法经典案例解析及应用实现,这份BAT大厂大数据项目实战PDF你一定要看看

  • java.util.concurrent.ScheduledThreadPoolExecutor

  • ScheduledThreadPoolExecutor} which is a thread pool for repeatedly

  • executing tasks at a given rate or delay. It is effectively a more

  • versatile replacement for the {@code Timer}/{@code TimerTask}

  • combination, as it allows multiple service threads, accepts various

  • time units, and doesn’t require subclassing {@code TimerTask} (just

  • implement {@code Runnable}). Configuring {@code

  • ScheduledThreadPoolExecutor} with one thread makes it equivalent to

  • {@code Timer}.

*/

public class Timer {

//。。。。

}

简单翻译下:ScheduledThreadPoolExecutor是一个具有更多功能的Timer的替代品,允许多个服务线程,如果设置一个服务线程和Timer没啥差别。其实从这段注释中已经能看出ScheduledThreadPoolExecutor的定位,通过注释看出相对于Timer,可能就是单线程跑任务和多线程跑任务的区别,我们从代码详细看下。

public class ScheduledThreadPoolExecutor

extends ThreadPoolExecutor

implements ScheduledExecutorService {

// …

public ScheduledThreadPoolExecutor(int corePoolSize) {

super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,

new DelayedWorkQueue());

}

private class ScheduledFutureTask

extends FutureTask implements RunnableScheduledFuture {

//…

}

}

从核心属性和构造器看,它继承了 ThreadPoolExecutor,实现了 ScheduledExecutorService,基本确认是基于线程池实现了延迟任务操作,我们知道线程池流程中有两个重要角色:任务,阻塞队列,ScheduledThreadPoolExecutor实现方案也是将这两者进行了重新设计:一个是 ScheduledFutureTask ,一个是 DelayedWorkQueue。

private class ScheduledFutureTask

extends FutureTask implements RunnableScheduledFuture {

//…

public void run() {

boolean periodic = isPeriodic();

if (!canRunInCurrentRunState(periodic))

cancel(false);

else if (!periodic)

ScheduledFutureTask.super.run(); //如果不是周期任务,直接run

else if (ScheduledFutureTask.super.runAndReset()) {//周期性任务,执行并重置任务状态为NEW

setNextRunTime();//先设置下次任务执行时间

reExecutePeriodic(outerTask); //任务重新入队列

}

}

//…

}

ScheduledFutureTask 继承自 FutureTask 重写了 run 方法,实现了周期性任务的场景。

static class DelayedWorkQueue extends AbstractQueue

implements BlockingQueue {

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)

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();

}

}

}

DelayedWorkQueue继承阻塞队列,实现了优先队列逻辑,本质也是利用数组实现的小顶堆,同时重写了take等方法,做了延迟阻塞处理。

现在我们来看下ScheduledThreadPoolExecutor提交一个任务后,整体的执行过程:

  • 提交一个任务后,为了满足ScheduledThreadPoolExecutor能够延时执行任务和能周期执行任务的特性,会先将实现Runnable接口的类转换成ScheduledFutureTask。

  • 然后会调用delayedExecute方法进行执行任务:先将任务放入到队列中,然后调用ensurePrestart方法,新建Worker类(此逻辑为线程池ThreadPoolExecutor实现)

  • 当执行任务时,就会调用被Worker所重写的run方法,进而会继续执行runWorker方法。在runWorker方法中会调用getTask方法从阻塞队列中不断的去获取任务进行执行,直到从阻塞队列中获取的任务为null的话,线程结束终止。(此处逻辑都是线程池ThreadPoolExecutor的实现)

    • getTask方法会调用队列的poll和take方法,此处就调用到DelayedWorkQueue重写的poll和take逻辑,实现了延迟任务的阻塞
  • 执行任务时,将调用ScheduledFutureTask重载的run方法,实现周期性任务的场景

小结:

  1. ScheduledThreadPoolExecutor继承了ThreadPoolExecutor,通过重写任务、阻塞队列实现了延迟任务调度的实现

  2. ScheduledThreadPoolExecutor大致的流程和Timer差不多,都是通过一个阻塞队列维护任务,能实现单次任务、周期性任务的执行,主要差别在于能多线程运行任务,不会单线程阻塞,并且Java线程池的底层runworker实现了异常的捕获,不会因为一个任务的出错而影响之后的任

  3. 在任务队列的维护上,与Timer一样,也是优先队列,插入和删除的时间复杂度是O(logn)

 DelayQueue

我们还可以通过DelayQueue配合线程池的方式实现延迟任务,看了下DelayQueue的源码,实现比较简单,实现逻辑和ScheduledThreadPoolExecutor中DelayedWorkQueue基本差不多,内部使用优先队列,队列元素为Delayed,Delayed接口继承了Comparable,优先队列通过Comparable进行排序。

//元素必须实现Delayed接口,也实现了阻塞队列public class DelayQueue extends AbstractQueue

implements BlockingQueue {

private final transient ReentrantLock lock = new ReentrantLock();

private final PriorityQueue q = new PriorityQueue();//优先队列,

public E take() throws InterruptedException {

final ReentrantLock lock = this.lock;

lock.lockInterruptibly();

try {

for (;😉 {

E first = q.peek();

if (first == null)

available.await();

else {

long delay = first.getDelay(NANOSECONDS);

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. 约定某个时间点执行

  3. 周期性执行。

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,移动指针找到槽位,根据轮次+时间来判断是否是需要处理的任务。

不足之处:

  1. 时间轮的推进是根据时间精度TickDuration来固定推进的,如果槽位中无任务,也需要移动指针,会造成无效的时间轮推进,比如TickDuration为1秒,此时就一个延迟500秒的任务,那就是有499次无用的推进。

  2. 任务的执行都是同一个工作线程处理的,并且工作线程的除了处理执行到时的任务还做了其他操作,因此任务不一定会被精准的执行,而且任务的执行如果不是新起一个线程执行,那么耗时的任务会阻塞下个任务的执行。

优势就是时间精度可控,并且增删任务的时间复杂度都是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

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img

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

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

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
img

面试结束复盘查漏补缺

每次面试都是检验自己知识与技术实力的一次机会,面试结束后建议大家及时总结复盘,查漏补缺,然后有针对性地进行学习,既能提高下一场面试的成功概率,还能增加自己的技术知识栈储备,可谓是一举两得。

以下最新总结的阿里P6资深Java必考题范围和答案,包含最全MySQL、Redis、Java并发编程等等面试题和答案,用于参考~

重要的事说三遍,关注+关注+关注!

历经30天,说说我的支付宝4面+美团4面+拼多多四面,侥幸全获Offer

image.png

更多笔记分享

历经30天,说说我的支付宝4面+美团4面+拼多多四面,侥幸全获Offer

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。**

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-kajvwvq0-1712003719558)]
[外链图片转存中…(img-Skt8O0sf-1712003719559)]
[外链图片转存中…(img-jxIJq8p6-1712003719559)]
[外链图片转存中…(img-A7XTu481-1712003719560)]
[外链图片转存中…(img-2hAlDhlb-1712003719560)]
[外链图片转存中…(img-1HeMJfAU-1712003719560)]

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

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

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-EahuKEob-1712003719561)]

面试结束复盘查漏补缺

每次面试都是检验自己知识与技术实力的一次机会,面试结束后建议大家及时总结复盘,查漏补缺,然后有针对性地进行学习,既能提高下一场面试的成功概率,还能增加自己的技术知识栈储备,可谓是一举两得。

以下最新总结的阿里P6资深Java必考题范围和答案,包含最全MySQL、Redis、Java并发编程等等面试题和答案,用于参考~

重要的事说三遍,关注+关注+关注!

[外链图片转存中…(img-6v0blnnu-1712003719561)]

[外链图片转存中…(img-oahn2cjb-1712003719561)]

更多笔记分享

[外链图片转存中…(img-Sm34gYdB-1712003719562)]

  • 22
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值