-
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方法,实现周期性任务的场景
小结:
-
ScheduledThreadPoolExecutor继承了ThreadPoolExecutor,通过重写任务、阻塞队列实现了延迟任务调度的实现
-
ScheduledThreadPoolExecutor大致的流程和Timer差不多,都是通过一个阻塞队列维护任务,能实现单次任务、周期性任务的执行,主要差别在于能多线程运行任务,不会单线程阻塞,并且Java线程池的底层runworker实现了异常的捕获,不会因为一个任务的出错而影响之后的任
-
在任务队列的维护上,与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,这两者之间可以相互转换,比如给你个任务,要求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
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
面试结束复盘查漏补缺
每次面试都是检验自己知识与技术实力的一次机会,面试结束后建议大家及时总结复盘,查漏补缺,然后有针对性地进行学习,既能提高下一场面试的成功概率,还能增加自己的技术知识栈储备,可谓是一举两得。
以下最新总结的阿里P6资深Java必考题范围和答案,包含最全MySQL、Redis、Java并发编程等等面试题和答案,用于参考~
重要的事说三遍,关注+关注+关注!
更多笔记分享
自我介绍一下,小编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)]