《从Java面试题看源码》-Flow、SubmissionPubliser源码分析_submissionpublisher 最大容量

/** 订阅者,供内部使用*/
static final class ConsumerSubscriber implements Subscriber {
final CompletableFuture status;
final Consumer<? super T> consumer;
Subscription subscription;
ConsumerSubscriber(CompletableFuture status,
Consumer<? super T> consumer) {
this.status = status; this.consumer = consumer;
}
public final void onSubscribe(Subscription subscription) {
this.subscription = subscription;
//如果元素已经消费完成,那么取消订阅
status.whenComplete((v, e) -> subscription.cancel());
//没有完成继续请求
if (!status.isDone())
subscription.request(Long.MAX_VALUE);
}
//发送错误时候的处理
public final void onError(Throwable ex) {
status.completeExceptionally(ex);
}
//元素消费完成时候的处理
public final void onComplete() {
status.complete(null);
}
//有新元素的时候会被调用
public final void onNext(T item) {
try {
//执行Consumer函数
consumer.accept(item);
} catch (Throwable ex) {
subscription.cancel();
status.completeExceptionally(ex);
}
}
}


##### ConsumerTask



//订阅者的消费任务
static final class ConsumerTask extends ForkJoinTask
implements Runnable, CompletableFuture.AsynchronousCompletionTask {
final BufferedSubscription consumer;
ConsumerTask(BufferedSubscription consumer) {
this.consumer = consumer;
}
public final Void getRawResult() { return null; }
public final void setRawResult(Void v) {}
public final boolean exec() { consumer.consume(); return false; }
public final void run() { consumer.consume(); }
}


##### BufferedSubscription



//基于数组的可扩展的环形缓冲区,通过CAS原子操作来实现插入和获取元素
//在任何时间内最多只有一个活动的消费者任务
//publisher通过锁来保证单个生产者
//生成者和消费者之间的同步依赖于volatile修饰的ctl、demand、waiting变量
//使用ctl管理状态
//缓存区开始很小,只在需要的时候扩容
//使用了Contended类避免CPU伪共享问题
//当生成者和消费者以不同的速度运行时,会出现失衡,通过人为细分一些消费者方法,包括隔离所有subscriber回调
@jdk.internal.vm.annotation.Contended
static final class BufferedSubscription
implements Subscription, ForkJoinPool.ManagedBlocker {
long timeout; // Long.MAX_VALUE 表示一直等待
int head; // 下一个获取的位置
int tail; // 下一个插入的位置
final int maxCapacity; // 最大缓冲大小
volatile int ctl; // 以volatile方式运行状态标志
Object[] array; // 缓冲数组
final Subscriber<? super T> subscriber; //消费者
final BiConsumer<? super Subscriber<? super T>, ? super Throwable> onNextHandler; //用于处理异常
Executor executor; // 线程池,发生错误为null
Thread waiter; // 生成者线程被阻塞
Throwable pendingError; // onError发生时会赋值该变量
BufferedSubscription next; // publisher使用
BufferedSubscription nextRetry; // publisher使用

@jdk.internal.vm.annotation.Contended("c") // 隔离CPU缓存行
volatile long demand;              // 未获取的请求
@jdk.internal.vm.annotation.Contended("c")
volatile int waiting;              // 如果生产者被阻塞,则不为零

// ctl 16进制值
static final int CLOSED   = 0x01;  // 设置了,忽略其他位
static final int ACTIVE   = 0x02;  // 保存消费者任务一直存活
static final int REQS     = 0x04;  // 非零请求
static final int ERROR    = 0x08;  // 发生错误时,调用onError
static final int COMPLETE = 0x10;  // 当完成时,调用onComplete
static final int RUN      = 0x20;  // 任务正在跑或刚开始跑
static final int OPEN     = 0x40;  // 订阅后位true

static final long INTERRUPTED = -1L; // 超时或者中断标志

BufferedSubscription(Subscriber<? super T> subscriber,
                     Executor executor,
                     BiConsumer<? super Subscriber<? super T>,

? super Throwable> onNextHandler,
Object[] array,
int maxBufferCapacity) {
this.subscriber = subscriber;
this.executor = executor;
this.onNextHandler = onNextHandler;
this.array = array;
this.maxCapacity = maxBufferCapacity;
}

// Wrappers for some VarHandle methods

//cas更新ctl变量
final boolean weakCasCtl(int cmp, int val) {
    return CTL.weakCompareAndSet(this, cmp, val);
}

//进行或计算
final int getAndBitwiseOrCtl(int bits) {
    return (int)CTL.getAndBitwiseOr(this, bits);
}

//这段代码会将demand的值减去指定的k
final long subtractDemand(int k) {
    long n = (long)(-k);
    //为什么还要再加n呢?
    return n + (long)DEMAND.getAndAdd(this, n);
}

//cas设置demand值
final boolean casDemand(long cmp, long val) {
    return DEMAND.compareAndSet(this, cmp, val);
}

// SubmissionPublisher实用方法

/\*\*

* 如果关闭了返回true (消费任务可能还在运行中).
*/
final boolean isClosed() {
return (ctl & CLOSED) != 0;
}

/\*\*

* 返回估计的元素数量,如果关闭了返回-1
*/
final int estimateLag() {
int c = ctl, n = tail - head;
return ((c & CLOSED) != 0) ? -1 : (n < 0) ? 0 : n;
}

// 提交元素的方法

/\*\*

* 尝试插入元素,并启动消费任务
* @return 如果关闭返回-1,0表示满了
*/
final int offer(T item, boolean unowned) {
//存放元素的数组
Object[] a;
//cap 表示数组的容量
int stat = 0, cap = ((a = array) == null) ? 0 : a.length;
//i 表示要插入的索引位置;n表示已有的元素
int t = tail, i = t & (cap - 1), n = t + 1 - head;
if (cap > 0) {
boolean added;
if (n >= cap && cap < maxCapacity) // 发生扩容
added = growAndOffer(item, a, t);
else if (n >= cap || unowned) // 如果不是自身的线程,就说明有竞争,需要使用CAS来更新
added = QA.compareAndSet(a, i, null, item);
else { // 使用release 模式插入元素到数组中
QA.setRelease(a, i, item);
added = true;
}
//插入成功,tail后移一位
if (added) {
tail = t + 1;
stat = n;
}
}
return startOnOffer(stat); //尝试唤醒消费线程
}

/\*\*

* 尝试扩展缓冲区,并插入元素,成功返回true。如果内存溢出就会失败
*/
final boolean growAndOffer(T item, Object[] a, int t) {
int cap = 0, newCap = 0;
Object[] newArray = null;
if (a != null && (cap = a.length) > 0 && (newCap = cap << 1) > 0) { //进行两倍扩容
try {
newArray = new Object[newCap];
} catch (OutOfMemoryError ex) {
}
}
if (newArray == null)
return false;
else {
//从原数组中取值,并插入新数组中
int newMask = newCap - 1;
newArray[t-- & newMask] = item;
for (int mask = cap - 1, k = mask; k >= 0; --k) {
Object x = QA.getAndSet(a, t & mask, null);
if (x == null) //x为null说明已经获取
break;
else
newArray[t-- & newMask] = x;
}
array = newArray;
VarHandle.releaseFence(); //释放空数组
return true;
}
}

/\*\*

* 重试版本,没有扩容或者偏置
*/
final int retryOffer(T item) {
Object[] a;
int stat = 0, t = tail, h = head, cap;
if ((a = array) != null && (cap = a.length) > 0 &&
QA.compareAndSet(a, (cap - 1) & t, null, item))
//将tail后移1位
stat = (tail = t + 1) - h;
return startOnOffer(stat);
}

/\*\*

* 唤醒消费者任务
* @return 如果是关闭的返回负数
*/
final int startOnOffer(int stat) {
int c; // 如果存在请求且未激活,则启动或保持活动状态
if (((c = ctl) & (REQS | ACTIVE)) == REQS &&
((c = getAndBitwiseOrCtl(RUN | ACTIVE)) & (RUN | CLOSED)) == 0)
//启动消费者线程
tryStart();
else if ((c & CLOSED) != 0)
stat = -1;
return stat;
}

/\*\*

* 启动消费的任务。 失败设置错误状态。
*/
final void tryStart() {
try {
Executor e;
//消费者任务
ConsumerTask task = new ConsumerTask(this);
if ((e = executor) != null) // 发生错误就跳过
e.execute(task);
} catch (RuntimeException | Error ex) {
//设置ctl状态
getAndBitwiseOrCtl(ERROR | CLOSED);
throw ex;
}
}

// 向消费者任务发信号

/\*\*

* 设置给定的控制位,启动任务,如果任务没有运行或者没有关闭
* @param 运行状态位
*/
final void startOnSignal(int bits) {
if ((ctl & bits) != bits &&
(getAndBitwiseOrCtl(bits) & (RUN | CLOSED)) == 0)
tryStart();
}

//订阅中
final void onSubscribe() {
    startOnSignal(RUN | ACTIVE);
}

//已完成
final void onComplete() {
    startOnSignal(RUN | ACTIVE | COMPLETE);
}

//发生错误
final void onError(Throwable ex) {
    int c; Object[] a;      //发生异步错误了清空缓冲区
    if (ex != null)
        pendingError = ex; 
    if (((c = getAndBitwiseOrCtl(ERROR | RUN | ACTIVE)) & CLOSED) == 0) {  //还没有关闭
        if ((c & RUN) == 0)  //没有运行,尝试重试
            tryStart();
        else if ((a = array) != null)  //清空缓冲区
            Arrays.fill(a, null);
    }
}

//取消
public final void cancel() {
    onError(null);
}

//请求为获取的数据
public final void request(long n) {
    if (n > 0L) {
        for (;;) {
            long p = demand, d = p + n;  // saturate
            if (casDemand(p, d < p ? Long.MAX_VALUE : d))
                break;
        }
        startOnSignal(RUN | ACTIVE | REQS);
    }
    else
        onError(new IllegalArgumentException(
                    "non-positive subscription request"));
}

// 消费者的方法

/\*\*

* ConsumerTask调用,循环消费元素,或者在提交元素的时候,间接触发
*/
final void consume() {
Subscriber<? super T> s;
if ((s = subscriber) != null) { // hoist checks
//触发消费者,请求更多的数据
subscribeOnOpen(s);
long d = demand;
//从数组头到尾进行循环
for (int h = head, t = tail;😉 {
int c, taken; boolean empty;
//如果发生了错误,中断消费
if (((c = ctl) & ERROR) != 0) {
closeOnError(s, null);
break;
}
//一直获取元素,直到消费完或者发生错误
else if ((taken = takeItems(s, d, h)) > 0) {
//头指针后移
head = h += taken;
//修改未获取数据的局部变量demand
d = subtractDemand(taken);
}
else if ((d = demand) == 0L && (c & REQS) != 0)
weakCasCtl(c, c & ~REQS); // 已经消费完,删除ctl中的请求标志位
else if (d != 0L && (c & REQS) == 0)
weakCasCtl(c, c | REQS); // 有新元素需要消费,增加ctl中的请求标志位
else if (t == (t = tail)) { // 稳定性检查
if ((empty = (t == h)) && (c & COMPLETE) != 0) { //已经消费完了,但缓存区还没关闭
closeOnComplete(s); //关闭缓存区
break;
}
else if (empty || d == 0L) {
//判断ctl运行标志是否有ACTIVE标志,有bit=ACTIVE
//没有bit=RUN
int bit = ((c & ACTIVE) != 0) ? ACTIVE : RUN;
//删除相应的标志位,并更新ctl值,如果是RUN状态,需要退出消费任务
if (weakCasCtl(c, c & ~bit) && bit == RUN)
break; // 取消激活或者退出
}
}
}
}
}

/\*\*

* 一直消费元素直到 不可用 或者 达到边界 或者 发生错误
*
* @param s subscriber 消费者
* @param d demand 当前需要获取的数量
* @param h current head 当前的头指针位置
* @return number taken 获取的数量
*/
final int takeItems(Subscriber<? super T> s, long d, int h) {
Object[] a;
int k = 0, cap;
if ((a = array) != null && (cap = a.length) > 0) {
int m = cap - 1, b = (m >>> 3) + 1; // min(1, cap/8)
int n = (d < (long)b) ? (int)d : b;
for (; k < n; ++h, ++k) {
//获取元素,并置为null
Object x = QA.getAndSet(a, h & m, null);
if (waiting != 0)
//唤醒被阻塞的生成者
signalWaiter();
if (x == null)
break;
else if (!consumeNext(s, x)) //调用消费者的onNext方法,来处理元素
break;
}
}
return k;
}

//调用消费者的onNext方法,来处理元素
final boolean consumeNext(Subscriber<? super T> s, Object x) {
    try {
        @SuppressWarnings("unchecked") T y = (T) x;
        if (s != null)
            s.onNext(y);
        return true;
    } catch (Throwable ex) {
        //处理异常
        handleOnNext(s, ex);
        return false;
    }
}

/\*\*

* 处理 Subscriber.onNext中的异常
*/
final void handleOnNext(Subscriber<? super T> s, Throwable ex) {
BiConsumer<? super Subscriber<? super T>, ? super Throwable> h;
try {
if ((h = onNextHandler) != null)
//调用自定义的BiConsumer函数处理异常
h.accept(s, ex);
} catch (Throwable ignore) {
}
//关闭缓冲并调用消费者的onError方法
closeOnError(s, ex);
}

/\*\*

* 如果是第一次执行,那么执行subscriber.onSubscribe,用于请求更多数据
*/
final void subscribeOnOpen(Subscriber<? super T> s) {
if ((ctl & OPEN) == 0 && (getAndBitwiseOrCtl(OPEN) & OPEN) == 0)
consumeSubscribe(s);
}

//执行subscriber.onSubscribe,用于请求更多数据
final void consumeSubscribe(Subscriber<? super T> s) {
    try {
        if (s != null) // ignore if disabled
            s.onSubscribe(this);
    } catch (Throwable ex) {
        closeOnError(s, ex);
    }
}

/\*\*

* 缓冲区还没关闭,就执行subscriber.onComplete
*/
final void closeOnComplete(Subscriber<? super T> s) {
if ((getAndBitwiseOrCtl(CLOSED) & CLOSED) == 0)
consumeComplete(s);
}

//执行subscriber.onComplete
final void consumeComplete(Subscriber<? super T> s) {
    try {
        if (s != null)
            s.onComplete();
    } catch (Throwable ignore) {
    }
}

/\*\*

* 发生错误,执行subscriber.onError,并且唤醒阻塞的生产者
*/
final void closeOnError(Subscriber<? super T> s, Throwable ex) {
if ((getAndBitwiseOrCtl(ERROR | CLOSED) & CLOSED) == 0) {
if (ex == null)
ex = pendingError;
pendingError = null; // detach
executor = null; // suppress racing start calls
signalWaiter();
consumeError(s, ex);
}
}

//执行subscriber.onError
final void consumeError(Subscriber<? super T> s, Throwable ex) {
    try {
        if (ex != null && s != null)
            s.onError(ex);
    } catch (Throwable ignore) {
    }
}

// 阻塞支持

/\*\*

* 唤醒等待的生产者
*/
final void signalWaiter() {
Thread w;
waiting = 0;
if ((w = waiter) != null)
LockSupport.unpark(w);
}

/\*\*

* 如果缓冲区关闭了,或者有可用空间,返回true
*/
public final boolean isReleasable() {
Object[] a; int cap;
return ((ctl & CLOSED) != 0 ||
((a = array) != null && (cap = a.length) > 0 &&
QA.getAcquire(a, (cap - 1) & tail) == null));
}

/\*\*

* 一直阻塞知道 timeout, closed,或者有可用空间
*/
final void awaitSpace(long nanos) {
if (!isReleasable()) {
ForkJoinPool.helpAsyncBlocker(executor, this);
if (!isReleasable()) {
timeout = nanos;
try {
ForkJoinPool.managedBlock(this);
} catch (InterruptedException ie) {
timeout = INTERRUPTED;
}
if (timeout == INTERRUPTED)
Thread.currentThread().interrupt();
}
}
}

/\*\*

* 给 ManagedBlocker提供的用于阻塞的方法
*/
public final boolean block() {
long nanos = timeout;
boolean timed = (nanos < Long.MAX_VALUE);
long deadline = timed ? System.nanoTime() + nanos : 0L;
while (!isReleasable()) {
if (Thread.interrupted()) {
timeout = INTERRUPTED;
if (timed)
break;
}
else if (timed && (nanos = deadline - System.nanoTime()) <= 0L)
break;
else if (waiter == null)
waiter = Thread.currentThread();
else if (waiting == 0)
waiting = 1;
else if (timed)
LockSupport.parkNanos(this, nanos);
else
LockSupport.park(this);
}
waiter = null;
waiting = 0;
return true;
}

// VarHandle mechanics 一些变量的封装对象,相当于以前调用UNSAFE的方法
static final VarHandle CTL;
static final VarHandle DEMAND;
static final VarHandle QA;

static {
    try {
        MethodHandles.Lookup l = MethodHandles.lookup();
        CTL = l.findVarHandle(BufferedSubscription.class, "ctl",
                              int.class);
        DEMAND = l.findVarHandle(BufferedSubscription.class, "demand",
                                 long.class);
        QA = MethodHandles.arrayElementVarHandle(Object[].class);
    } catch (ReflectiveOperationException e) {
        throw new ExceptionInInitializerError(e);
    }

    // 减少首次加载时候的风险
    // LockSupport.park: https://bugs.openjdk.java.net/browse/JDK-8074773
    Class<?> ensureLoaded = LockSupport.class;
}

}


##### ThreadPerTaskExecutor



/** 如果ForkJoinPool.commonPool()不支持并发(公共线程池的并发级别小于1),将使用该类*/
private static final class ThreadPerTaskExecutor implements Executor {
ThreadPerTaskExecutor() {} // 禁止使用构造函数创建类
public void execute(Runnable r) { new Thread®.start(); }
}


#### 基本方法


##### subscribe



//给传入的订阅者订阅,如果已经订阅,将调用订阅者的onError方法抛出IllegalStateException异常
//如果订阅成功,则会异步调用订阅者的onSubscribe方法,如果其中抛出异常,订阅将被取消
//如果SubmissionPublisher被异常关闭,那么订阅者的onError方法会被调用
//如果没有异常被关闭了,就会调用订阅者的onComplete方法
//通过调用Subscription的request方法能够接收其他更多数据
//通过调用Subscription的cancel方法取消订阅
public void subscribe(Subscriber<? super T> subscriber) {
if (subscriber == null) throw new NullPointerException();
// 分配初始数组
int max = maxBufferCapacity;
Object[] array = new Object[max < INITIAL_CAPACITY ?
max : INITIAL_CAPACITY];
BufferedSubscription subscription =
new BufferedSubscription(subscriber, executor, onNextHandler,
array, max);
//同步解决了一些变量可见性问题
synchronized (this) {
if (!subscribed) {
subscribed = true;
owner = Thread.currentThread();
}
//循环缓冲区链表,并处理订阅
for (BufferedSubscription b = clients, pred = null;😉 {
//新的订阅者,始终会被放到链表的最后,b的变量会在循环末尾被next赋值,最后一个缓冲区的next值为null
if (b == null) {
Throwable ex;
//执行消费者任务
subscription.onSubscribe();
if ((ex = closedException) != null)
//异常处理
subscription.onError(ex);
else if (closed)
//关闭完成处理
subscription.onComplete();
else if (pred == null)
//首次订阅
clients = subscription;
else
//后面的订阅者缓冲区通过next连接成链表
pred.next = subscription;
break;
}
BufferedSubscription next = b.next;
if (b.isClosed()) { // 缓冲区关闭
b.next = null; // 断开该缓存区的连接
if (pred == null)
clients = next;
else
pred.next = next;
}
else if (subscriber.equals(b.subscriber)) { //重复的订阅者,抛出异常
b.onError(new IllegalStateException(“Duplicate subscribe”));
break;
}
else
pred = b;
b = next;
}
}
}


##### submit



//将元素发布给每个订阅者,如果缓冲区已满,也会一直等待
public int submit(T item) {
return doOffer(item, Long.MAX_VALUE, null);
}


##### offer



//提供了自定义BiPredicate用于判断,参数是否满足指定的要求,如果满足将有一次重试的机会
//该方法如果缓存区已满,不会一直等待
public int offer(T item,
BiPredicate<Subscriber<? super T>, ? super T> onDrop) {
return doOffer(item, 0L, onDrop);
}

//跟前者比,多提供了等待超时时间,和时间单位
public int offer(T item, long timeout, TimeUnit unit,
BiPredicate<Subscriber<? super T>, ? super T> onDrop) {
long nanos = unit.toNanos(timeout);
// distinguishes from untimed (only wrt interrupt policy)
if (nanos == Long.MAX_VALUE) --nanos;
return doOffer(item, nanos, onDrop);
}


##### close



//给当前的订阅者发布onComplete信号
//并禁止后面的发布任务
//该方法无法说明所有的订阅者已经完成
public void close() {
if (!closed) {
BufferedSubscription b;
synchronized (this) {
// no need to re-check closed here
b = clients;
clients = null;
owner = null;
closed = true;
}
//循环处理所有的缓冲区,断开连接,完成消费
while (b != null) {
BufferedSubscription next = b.next;
b.next = null;
b.onComplete();
b = next;
}
}
}


##### closeExceptionally



//给当前的订阅者发送指定的错误信号,并禁止后续发布
//该方法无法说明订阅者是否已经完成
public void closeExceptionally(Throwable error) {
if (error == null)
throw new NullPointerException();
if (!closed) {
BufferedSubscription b;
synchronized (this) {
b = clients;
if (!closed) { //再次检查关闭状态,因为有可能其他线程会调用close()
closedException = error;
clients = null;
owner = null;
closed = true;
}
}
while (b != null) {
BufferedSubscription next = b.next;
b.next = null;
b.onError(error);
b = next;
}
}
}


##### isClosed



//是否关闭
public boolean isClosed() {
return closed;
}


##### getClosedException



//获取closed异常
public Throwable getClosedException() {
return closedException;
}


##### hasSubscribers



//判断是否有订阅者,只要有一个订阅者就返回true
//如果有缓冲区已经被关闭,会清理这些缓冲区
public boolean hasSubscribers() {
boolean nonEmpty = false;
synchronized (this) {
for (BufferedSubscription b = clients; b != null;) {
BufferedSubscription next = b.next;
if (b.isClosed()) {
b.next = null;
b = clients = next;
}
else {
nonEmpty = true;
break;
}
}
}
return nonEmpty;
}


##### getNumberOfSubscribers



//通过缓冲区来计算还订阅的订阅者数量
public int getNumberOfSubscribers() {
synchronized (this) {
return cleanAndCount();
}
}


##### getExecutor



//获取线程池
public Executor getExecutor() {
return executor;
}


##### getMaxBufferCapacity



//获取缓冲区最大容量
public int getMaxBufferCapacity() {
return maxBufferCapacity;
}


##### getSubscribers



//获取还订阅的订阅者列表,并清理缓冲区链表
public List<Subscriber<? super T>> getSubscribers() {
ArrayList<Subscriber<? super T>> subs = new ArrayList<>();
synchronized (this) {
BufferedSubscription pred = null, next;
for (BufferedSubscription b = clients; b != null; b = next) {
next = b.next;
if (b.isClosed()) {
b.next = null;
if (pred == null)
clients = next;
else
pred.next = next;
}
else {
subs.add(b.subscriber);
pred = b;
}
}
}
return subs;
}


##### isSubscribed



//通过equals判断,指定的订阅者是否还在订阅
public boolean isSubscribed(Subscriber<? super T> subscriber) {
if (subscriber == null) throw new NullPointerException();
if (!closed) {
synchronized (this) {
BufferedSubscription pred = null, next;
for (BufferedSubscription b = clients; b != null; b = next) {
next = b.next;
if (b.isClosed()) {
b.next = null;

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

ubscriber);
pred = b;
}
}
}
return subs;
}


##### isSubscribed



//通过equals判断,指定的订阅者是否还在订阅
public boolean isSubscribed(Subscriber<? super T> subscriber) {
if (subscriber == null) throw new NullPointerException();
if (!closed) {
synchronized (this) {
BufferedSubscription pred = null, next;
for (BufferedSubscription b = clients; b != null; b = next) {
next = b.next;
if (b.isClosed()) {
b.next = null;

[外链图片转存中…(img-GXorVpii-1714165353702)]
[外链图片转存中…(img-p2OVOwXt-1714165353703)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值