最新要闻
- 全球限量5000台!ThinkPad X1 Carbon 30周年纪念版即将上市
- 最新资讯:老马也失蹄 吞剑大师嘴插5把剑出意外被刺穿内脏:宣布收山
- NASA毅力号火星车和国际空间站上的SSD:出自群联之手
- 网友不知情下话费暴涨2倍至99元 客服:一般会提前通知
- 《阿凡达》为何时隔13年推出续作?卡梅隆回应:光剧本就写了4版
- 全球时讯:纯电越野车真香!奇瑞也来参一脚:邀网友共创、最多奖励5万
- 每日视讯:一天1500元 索赔77万元代步车费用!博主曝特斯拉“精彩”上诉状
- 黄牛栽了!急于出手RTX 4080
- 两轮电动车在印度卷起来了:5年/6万公里质保、1年免费充电
- 天天短讯!女子连续服用6片感冒药致肝衰竭 专家提醒:药不能随便吃
- 男子被狗舔伤口发视频炫耀狗子贴心:听网友劝后打狂犬疫苗
- 全球播报:乐视诉清华大学获赔:内网提供电影下载 7年前就关停了
- 国服腾讯、网易等谁来代理?暴雪《暗黑4》发售时间曝光:容量80GB
- 环球动态:男子醉驾撞树想溜 爱车自动报警 并发送了定位
- 当前热点-灵感来自微信:微软计划开发一站式“超级应用”
- 环球即时看!惊险!SUV被货车顶上铁轨 火车驶来瞬间逃离
手机
警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
票房这么火爆,如何请视障人士“看”一场电影?
- 警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
- 男子被关545天申国赔:获赔18万多 驳回精神抚慰金
- 3天内26名本土感染者,辽宁确诊人数已超安徽
- 广西柳州一男子因纠纷杀害三人后自首
- 洱海坠机4名机组人员被批准为烈士 数千干部群众悼念
- 票房这么火爆,如何请视障人士“看”一场电影?
家电
CompletableFuture源码解析
前言
JDK8 为我们带来了 CompletableFuture
这个有意思的新类,它提供比 Future
更灵活更强大的回调功能,借助 CompletableFuture
我们可以更方便的编排异步任务。
(相关资料图)
本着知其然也要知其所以然的想法,笔者结合源码深入了解了一下 CompletableFuture
的部分实现,然后写了这边文章作为总结。
一、数据结构
1、CompletableFuture
CompletableFuture
实现了 Future
接口和 CompletionStage
,Future
不必多说,而 CompletionStage
定义了各种任务编排的 API:
CompletableFuture implements Future, CompletionStage { volatile Object result; // Either the result or boxed AltResult volatile Completion stack; // Top of Treiber stack of dependent actions}
CompletableFuture
的数据结构包括用于记录结果的 result
,以及用于持有任务以及任务间依赖关系的 Completion
类型的成员变量 stack
。
如果阅读过 spring 注解相关功能的源码的同学,对于 CompletableFuture
和 Completion
应该会有一种 TypeMappedAnnotation
和 AnnotationTypeMapping
的既视感,实际上他们两者之间的关系确实也非常相似。
2、Completion
数据结构
Completion
是 CompletableFuture
中的一个内部类,我们可以简单的认为它就是我们一般所说的“操作”。
abstract static class Completion extends ForkJoinTask implements Runnable, AsynchronousCompletionTask { volatile Completion next;}
它通过 next
指针在 CompletableFuture
中形成一个链表结构。
依赖关系
它还有两个抽象的实现类 UniCompletion
和 BiCompletion
:
abstract static class UniCompletion extends Completion { Executor executor; // executor to use (null if none) CompletableFuture dep; // the dependent to complete CompletableFuture src; // source for action}abstract static class BiCompletion extends UniCompletion { CompletableFuture snd; // second source for action}
其中 executor
表示该操作的执行者,而 src
和 snd
两个指针表示要执行的操作对应的 CompletableFuture
实例,而 dep
则表示要执行的操作依赖的前置操作对应的 CompletableFuture
实例。多个 Completion
彼此之间通过这些指针维护彼此的依赖关系。
实现类
在 CompletableFuture
,我们会看到很多格式为 UniXXX
或者 BiXXX
的内部类,它们大多数都基于上述两抽象类实现,分别对应不同的操作。我们以 UniApply
为例:
static final class UniApply extends UniCompletion { Function super T,? extends V> fn;}
其本质上就是一个额外挂载了 Function
接口的 UniCompletion
,同理,XXXAccept
就是挂载了 Consumer
的 Completion
,而 XXXRun
就是挂载的 Runnable
接口的 Completion
。
二、构建流程
对 CompletableFuture
和 Completion
的数据结构有了基本的概念以后,我们一个简单任务的构建-执行过程来分析以下源码。
假设现在有两个异步任务 task1 与 task2,task2 需要在 task1 执行完毕后再执行:
CompletableFuture task1 = new CompletableFuture<>();CompletableFuture task2 = task1.thenApplyAsync(s -> s + " 2");
thenApplyAsync
本身提供两个方法,唯一的区别在于后者需要指定线程池,而前者使用默认的线程池:
public CompletableFuture thenApplyAsync( Function super T,? extends U> fn) { return uniApplyStage(asyncPool, fn);}public CompletableFuture thenApplyAsync( Function super T,? extends U> fn, Executor executor) { return uniApplyStage(screenExecutor(executor), fn);}
它们都需要通过 uniApplyStage
方法完成新任务的构建:
private CompletableFuture uniApplyStage( Executor e, Function super T,? extends V> f) { if (f == null) throw new NullPointerException(); // 1、构建一个 CompletableFuture,对应下一个任务 Task2 CompletableFuture d = new CompletableFuture(); if (e != null || !d.uniApply(this, f, null)) { // 2、构建一个 Completion,dep 指向 Task2,src 指向 Task1 UniApply c = new UniApply(e, d, this, f); // 3、将该 Completion 压入当前 CompletableFuture 栈顶 push(c); // 4、尝试以异步模式执行该Completion c.tryFire(SYNC); } return d;}final void push(UniCompletion,?> c) { if (c != null) { while (result == null && !tryPushStack(c)) lazySetNext(c, null); // clear on failure }}final boolean tryPushStack(Completion c) { Completion h = stack; // 将 c.next 设置为当前 Task1 持有的 Completion lazySetNext(c, h); // CAS,Task1 持有的 Completion 替换为 c return UNSAFE.compareAndSwapObject(this, STACK, h, c);}static void lazySetNext(Completion c, Completion next) { // 通过 CAS 把 c.next 指向 next UNSAFE.putOrderedObject(c, NEXT, next);}
uniApplyStage
方法做了四件事:
- 若将当前任务作为
Task1
,则会为下一个任务构建一个新的CompletableFuture
,姑且称为Task2
; - 若将
Task1
持有的Completion
称为C1
,则创建一个UniApply
类型的Completion
C2
,其中C2
dep
指向Task2
,src
指向Task1
; - 将
C2
的next
指向C1
,然后通过 CAS 将Task1
持有的C1
替换为C2
; - 尝试执行
C2
;
1、多级任务的构建流程
步骤 2 和 3 算是一个组合操作,它完成了创建 Completion
- 压入当前 CompletableFuture
栈的操作。
我们在原本示例中的 Task1
和 Task2
的基础上再最加一个 Task3
,这样或许会更有助于了解这一过程。
CompletableFuture task1 = new CompletableFuture<>();CompletableFuture task2 = task1.thenApplyAsync(s -> s + " 2");CompletableFuture task2 = task2.thenApplyAsync(s -> s + " 3");
首先,在不考虑 Task2
和 Task3
在构建过程中就完成的情况下,会有如下过程:
第一个任务
最开始,Task1
被创建,此时 Task1
是个空任务,它的 stack
和 result
都为 null
,为了便于理解,我们姑且认为它在 stack
指向一个虚拟的空 Completion
,称其为 c1
。
第二个任务
接着,task2
通过 task1.thenApplyAsync
方法被创建,此时:
- 一个新的
Completion
被创建,我们称其为c2
,c2
的dep
指向task2
,src
指向task1
; c2
的next
指向c1
;task1
的stack
从指向c1
变成指向c2
;
第三个任务
然后,task3
通过task2.thenApplyAsync
方法被创建,此时:
- 一个新的
Completion
被创建,我们称其为c3
,c3
的dep
指向task3
,src
指向task2
; c3
的next
指向c2
;task2
的stack
从指向c2
变成指向c3
;
2、平级任务的构建流程
如果这个时候我们再回头往 Task1
上追加一个与 Task2
平级的任务 Task4
呢?
CompletableFuture task1 = new CompletableFuture<>();CompletableFuture task2 = task1.thenApplyAsync(s -> s + " 2");CompletableFuture task2 = task2.thenApplyAsync(s -> s + " 3"); CompletableFuture task4 = task1.thenApplyAsync(s -> s + " 4");
- 一个新的
Completion
被创建,我们称其为c4
,c4
的dep
指向task4
,src
指向task1
; c4
的next
指向c2
;task2
的stack
从指向c2
变成指向c4
;
3、整体结构
至此,我们可以总结出一些信息:
CompletableFuture
所谓的栈,其实就是 Completion
的先进后出队列,假设现在有一个头结点 a
,调用 thenApply
方法将会向 a
之前追加一个新的头结点 b
,然后持有 a
的 CompletableFuture
转而去持有 b
,这样就永远可以通过持有的头结点遍历获取队列中的所有节点;
而当我们调用 thenApply
时,都会创建一个 Completion
,Completion
的 src
总是指向被调用 thenApply
方法的 CompletableFuture
,换而言之,Completion
被压入谁的栈,则 Completion.src
就指向谁。
基于上述逻辑,我们再对上面这张图进行简化,忽略栈中 Completion
间的 next
指针与指向栈所有者的 src
指针,则有:
这样看结构就非常清晰了,同理,如果 Task2
或者 Task4
也有多个后续任务,则这里就会变成一个多叉树结构,反之,若 Task2
或者 Task4
有多个 src
(比如调用了 thenCombine
方法)则就可能会变成一张图。
三、执行流程
我们依然回顾 uniApplyStage
这个方法:
private CompletableFuture uniApplyStage( Executor e, Function super T,? extends V> f) { if (f == null) throw new NullPointerException(); // 构建新的 CompletableFuture CompletableFuture d = new CompletableFuture(); // 如果是个异步任务,或者是个同步任务但是还没完成才进入判断 if (e != null || !d.uniApply(this, f, null)) { // 构建 Completion,dep 指向新 CompletableFuture,src 指向 this UniApply c = new UniApply(e, d, this, f); // 将新的 Complection 压入 this 的栈中 push(c); // 尝试执行新的 Complection c.tryFire(SYNC); } return d;}
实际上当我们调用 thenXXX
的时候,新的任务就已经在尝试执行了,接下来我们继续以 UniApply
为例,分析 Completion
的执行流程。
1、执行CompletableFuture
在 uniApplyStage
中,可以看到当新的 CompletableFuture
创建后,若该任务未指定 executor
,即这是一个同步的任务,则在 !d.uniApply(this, f, null)
这段代码先执行一次 uniApply
方法,也就是直接尝试执行用户指定的逻辑:
final boolean uniApply(CompletableFuture a, // 源任务,即若 this 为 Task2,则 a 为 Task1 Function super S,? extends T> f, UniApply c) { Object r;Throwable x; // 1、Task1没完成,就直接返回false if (a == null || (r = a.result) == null || f == null) return false; // 2、如果 Task1 已经完成,并且 Task1 抛出异常了,那么 this 就没必要执行了,也直接抛异常结束 tryComplete: if (result == null) { if (r instanceof AltResult) { if ((x = ((AltResult)r).ex) != null) { completeThrowable(x, r); break tryComplete; } r = null; } // 3、如果 Task1 已经完成了,并且没抛出异常,那么直接执行 this try { if (c != null && !c.claim()) return false; @SuppressWarnings("unchecked") S s = (S) r; // 3.1 执行成功,将结果记录到 this.result completeValue(f.apply(s)); } catch (Throwable ex) { // 3.2 执行失败,将异常封装一下也作为一个结果记录到 this.result completeThrowable(ex); } } return true;}
uniApply
主要逻辑如下:
- 如果源任务未完成,则什么都不做,直接返回
false
; - 如果源任务发生了异常,那么让当前任务也变为完成,并把源任务的结果(异常)作为当前任务的结果;
- 如果源任务已经正常完成,则获取源任务的结果,然后再将其作为输入参数执行当前任务,并且记录任务的执行结果;
这个方法实际上就是执行 Completion
挂载的用户业务逻辑的代码,由于考虑到源任务有可能是个异步任务,当尝试执行子任务的时候源任务还没完成,因此这个方法在后续实际上会被调用多次。
而 uniApplyStage
在没有指定 executor
是默认它就是一个同步任务,因此会直接在创建新的 CompletableFuture
的时候就执行一次,如果直接完成那后续也不需要再创建 Completion
了。
此外,在这里,我们可以很清楚的看到,发生异常的任务也被视为已完成,异常本身也被看成一个任务的执行结果。
2、执行Completion
tryFire
方法是 Completion
的执行触发点,他会尝试执行当前的 Completion
,并在完成后触发 dep
指向的 CompletableFuture
中,栈里面的 Completion
的执行。
执行模式
在看 tryFire
方法前,我们需要先简单了解一下 mode
参数,它表示 tryFire
时的执行模式,默认提供三个选项值:
static final int SYNC = 0; // 同步执行static final int ASYNC = 1; // 异步执行static final int NESTED = -1; // 嵌套执行,即 CompletableFuture 在递归中执行栈内的 Completion
SYNC
和 ASYNC
没什么可介绍的,CompletableFuture
中大部分的 Completion
都是以 SYNC
模式执行的。
不过在接下来的代码中,我们需要重点关注 mode < 0
这类判断,它涉及到栈中 Completion
的递归执行。
tryFire
tryFire
用于主动触发一个 Completion
的执行,但与 CompletableFutrue
的 uniApplyStage
方法不同的是,它还会处理源任务和子任务栈中的其他任务:
final CompletableFuture tryFire(int mode) { CompletableFuture d; // 子任务 (this.dep) CompletableFuture a; // 源任务 (this.src) // 没有后续的子任务, 或者有子任务但是当前任务执行失败了 if ((d = dep) == null || !d.uniApply(a = src, fn, mode > 0 ? null : this)) return null; // 2、置空相关属性,表示当前 Completion 已完成 dep = null; src = null; fn = null; // 3、则尝试把 dep 中栈里的 Completion 出栈,压入 src 的栈并执行 return d.postFire(a, mode);}
3、执行关联Completion
在 tryFire
后,当前 Completion
就实际完成了,接着就需要处理 Completion
的 dep
指向的 CompletableFuture
的栈内的哪些子任务,对应到代码就是调用 dep
的 postFire
方法,然后再在 dep
中调用 postComplete
方法:
final CompletableFuture postFire( CompletableFuture> a, // 当前任务的源任务,即 src 指向大 CompletableFuture int mode) { // 1、源任务的栈不为空 if (a != null && a.stack != null) { // 1.1 处于递归执行过程,或者源任务未完成,先清除栈中已经完成的任务 if (mode < 0 || a.result == null) a.cleanStack(); else // 1.2 不处于递归过程,且源任务已完成,将栈中的任务出栈并完成 a.postComplete(); } // 2、当前子任务已完成,且当前子任务的栈不为空 if (result != null && stack != null) { // 2.1 如果处于递归过程,就直接返回子任务本身 if (mode < 0) return this; // 2.2 如果不处于递归过程,则将子任务栈中的任务出栈并完成 else postComplete(); } return null;}
而在 postComplete
中,当发现源任务或者子任务完成时,会将当前源任务或者子任务的栈中全部任务都出栈,并尝试执行:
final void postComplete() { /* * On each step, variable f holds current dependents to pop * and run. It is extended along only one path at a time, * pushing others to avoid unbounded recursion. */ CompletableFuture> f = this; Completion h; // 递归直到当前任务以及dep的栈都为空为止 while ((h = f.stack) != null || (f != this && (h = (f = this).stack) != null)) { CompletableFuture> d; Completion t; if (f.casStack(h, t = h.next)) { // 将 f 的栈顶任务 h 出栈 // 1、h 还不是 f 栈中的最后一个任务 if (t != null) { if (f != this) { pushStack(h); // 将 f 的栈顶任务 h 压入 this 的栈中 continue; } h.next = null; // detach } // 2、h 已经是 f 栈中的最后一个任务了, // 直接执行任务 h,并让 f 指向该任务的 dep,然后再次循环 f = (d = h.tryFire(NESTED)) == null ? this : d; } }}
tryFire -> postFire -> postComplete -> tryFire......
构成了一个递归的过程,光看代码可能不是很直观,我们举个例子:
CompletableFuture task2 = task1.thenApply(t -> { System.out.println("2"); return "2"; });task2.thenAccept(t -> System.out.println("2.1"));task2.thenAccept(t -> System.out.println("2.2")) .thenAccept(t -> System.out.println("2.2.1")) .thenAccept(t -> System.out.println("2.2.1.1"));CompletableFuture task3 = task1.thenApply(t -> { System.out.println("3"); return "3"; });task3.thenAccept(t -> System.out.println("3.1"));task3.thenAccept(t -> System.out.println("3.2")) .thenAccept(t -> System.out.println("3.2.1")) .thenAccept(t -> System.out.println("3.2.1.1"));CompletableFuture task4 = task1.thenApply(t -> { System.out.println("4"); return "4"; });task1.complete("1");// 控制台输出// 4// 3// 3.1// 3.2// 3.2.1// 3.2.1.1// 2// 2.1// 2.2// 2.2.1// 2.2.1.1
根据上述代码的输出,我们可以很直观的意识到,在以 task1
为根节点的树结构中,各个任务的调用过程实际上就是深度优先遍历的过程,以被调用 postComplete
方法的 Completion
为根节点,会将其 dep
对应的 CompletableFuture
栈中的 Completion
弹出并压入根 Completion
的栈,然后执行并从夫上述过程。、
接下来我们结合上述代码,简单验证一下这个思路:
**task4
分支的执行 **
在最开始,task1
的栈中从上到下存放有 4、3、2 三个 Completion
:
首先,将 task1
的栈顶元素 4 出栈并执行,由于 4 的 dep
没有指向任何 CompletableFuture
,因此 4 这个分支全部的 Completion
都执行完毕。
现在,task1
的栈目前还有 3 和 2 两个 Completion
。
task3
分支的执行
将 task1
的栈顶元素 3 出栈并执行,由于 3 的 dep
指向了另一个 CompletableFuture
,该 CompletableFuture
的栈中存放有 3.2、3.1 两个 Completion
,所以:
- 先将栈顶元素 3.2 出栈,由于 3.2 不为栈中的最后一个元素,因此将 3.2 压入
task1
的栈顶; - 再将栈顶元素 3.1 出栈,由于 3.1 已经是栈中最后一个元素,因此直接执行 3.1;
此时 task1
中的栈情况如下:
然后将 task1
的栈顶元素 3.2 出栈并执行,由于 3.2 的 dep
指向的另一个 CompletableFuture
,该 CompletableFuture
的栈中存放有 3.2.1 ,因此将 3.2.1 出栈,又由于 3.2.1 已经是栈中最后一个元素,因此直接将其执行并返回 dep
。
由于 3.2.1 的 dep
指向了另一个 CompletableFuture
,该 CompletableFuture
的栈中存放有 3.2.1.1 ,因此将 3.2.1.1 出栈,又由于 3.2.1.1 已经是栈中最后一个元素,因此直接将其执行并返回 dep
。
3.2.1.1 的 dep
没有指向任何 CompletableFuture
,说明此时 3.2
这条分支上的所有栈都已经清空,此轮执行结束。
现在,task1
的栈目前只剩 2 一个 Completion
。
task2
分支的执行
task2
分支的执行与 task3
完全一致,因此这里只简单的说明:
- 弹出
task1
栈顶元素 2 并执行,返回dep
指向的CompletableFuture
; - 该
CompletableFuture
栈存在 2.2 与 2.1 两个Completion
:- 先弹出 2.2,由于 2.2 不是栈中最后一个元素,因此将其压入
task1
的栈; - 再弹出 2.1,由于 2.1 已经是栈中最后一个元素,因此将其直接执行;
- 先弹出 2.2,由于 2.2 不是栈中最后一个元素,因此将其压入
- 弹出
task1
栈顶元素 2.1 并执行,返回dep
指向的CompletableFuture
; - 该
CompletableFuture
栈仅存在 2.2.1 一个Completion
,因此直接执行并返回dep
指向的CompletableFuture
; - 该
CompletableFuture
栈仅存在 2.2.1.1 一个Completion
,因此直接执行并返回dep
指向的CompletableFuture
; - 由于 2.2.1.1 的
dep
没有指向任何CompletableFuture
,因此递归到这里就结束了。
总结
到这里,CompletableFuture
的构建-执行过程也基本讲完了。回顾整篇文章,不难发现其实大部分内容其实还是在说明以 CompletableFuture
和 Completion
为基础构建出来的数据结构。
每个 CompletableFuture
都会持有一个 Completion
栈,当我们向一个 CompletableFuture
追加任务时,本质上就是生成一个 Completion
并压入到栈中。而每个 Completion
则关联到另一个 CompletableFuture
,该 CompletableFuture
对应此 Completion
的完成状态。
明白这一点后,我们对 CompletableFuture
和 Completion
的定位就会有更加清晰的了解,如果我们将整个复杂异步流程视为树或者图,那么 CompletableFuture
和 Completion
实际上就是对应着点和边。当我们执行一个 CompletableFuture
,实际上就是基于关联的 Completion
路径遍历所有的 CompletableFuture
。
当然,实际上由于 CompletableFuture
执行 either
、any
、both
、all
等模式,因此实际在执行的时候还会有更多的判断逻辑,不过数据结构是不会变的。
CompletableFuture源码解析
全球限量5000台!ThinkPad X1 Carbon 30周年纪念版即将上市
最新资讯:老马也失蹄 吞剑大师嘴插5把剑出意外被刺穿内脏:宣布收山
NASA毅力号火星车和国际空间站上的SSD:出自群联之手
网友不知情下话费暴涨2倍至99元 客服:一般会提前通知
每日快讯!Redis配置、优化及相关命令
《阿凡达》为何时隔13年推出续作?卡梅隆回应:光剧本就写了4版
全球时讯:纯电越野车真香!奇瑞也来参一脚:邀网友共创、最多奖励5万
每日视讯:一天1500元 索赔77万元代步车费用!博主曝特斯拉“精彩”上诉状
黄牛栽了!急于出手RTX 4080
两轮电动车在印度卷起来了:5年/6万公里质保、1年免费充电
Power BI 15 DAY
【高精密时钟】NTP网络校时服务器在WIN平台下调试步骤
微头条丨Chatgpt注册全流程教程
KMP算法详解-字符串匹配
焦点信息:精美的web前端源码的特效
天天短讯!女子连续服用6片感冒药致肝衰竭 专家提醒:药不能随便吃
男子被狗舔伤口发视频炫耀狗子贴心:听网友劝后打狂犬疫苗
全球播报:乐视诉清华大学获赔:内网提供电影下载 7年前就关停了
国服腾讯、网易等谁来代理?暴雪《暗黑4》发售时间曝光:容量80GB
环球动态:男子醉驾撞树想溜 爱车自动报警 并发送了定位
全球热点!JS设计模式 之 发布-订阅模式
最近沉迷Redis网络模型,无法自拔!终于知道Redis为啥这么快了
热点聚焦:行为管理(锐捷行业网关篇)
全球时讯:文盘Rust -- r2d2 实现redis连接池
前端精准测试实践
世界速递!JavaScript中 FileReader 对象详解
【世界报资讯】大数据-数据仓库-实时数仓架构分析
热头条丨JAVA8 函数式编程(1)- Lambda表达式
易基因|m6A去甲基化酶ALKBH5通过降低PHF20 mRNA甲基化抑制结直肠癌进展 | 肿瘤研究
焦点日报:火山引擎 DataTester:如何用 A/B 测试做产品增长?
WTM+InfluxDB时序数据库数据查询并放到DataTable中
中科慧政 & JNPF :全面开启智慧政务,灵活满足政务办公需求
【焦点热闻】南墙WAF-最好的免费Web应用防火墙
天天关注:高光时刻 | 方正璞华联合开发的「人力资源法律服务共享平台」在创新创业大赛中获奖
全球新消息丨95年属猪的2019年运势
【环球热闻】绳责的意思(绳责)
天天动态:水滴筹标题范文(水滴筹标题怎么写)
今头条!应用昆虫学(应用昆虫学报)
环球头条:工厂找哪个网站?
焦点信息:促排卵期间注意哪些事项(促排卵期间注意事项)
天天资讯:越南旅游签证办理流程及费用(越南旅游签证办理流程)
全球快报:空鼻症是什么样(空鼻症是什么病)
【天天新视野】电脑桌面怎么恢复到原来的样子(电脑桌面怎么恢复到原来的样子)
每日信息:闲鱼卖二手
微资讯!西藏万隆虫草鹿鞭王
环球观焦点:77电玩城(77dizhi)
热头条丨低温性能革命性进步!宁德时代:钠电池有望装车500公里续航车型
当前播报:非典是哪一年一共死多少人(非典是哪一年)
天天热讯:空调显示屏上显示df是什么意思(空调运行中显示屏出现字母df是什么意思)
全球报道:我国现存新能源汽车相关企业56.8万家,仅2021年新增17万家
全球要闻:日本电子巨头罗姆将量产下一代半导体:提高用电效率、增加电动车续航里程
环球今日报丨传感器和处理器如何打造更智能、更自主的机器人?
世界即时:如何实现工业自动化?传感器对于工业自动化有什么样的意义
天天新动态:工业自动化如何实现?
当前焦点!使用cpolar(内网穿透)最低成本搭建网站
如何在Windows AD域中驻留ACL后门
当前热点-灵感来自微信:微软计划开发一站式“超级应用”
环球即时看!惊险!SUV被货车顶上铁轨 火车驶来瞬间逃离
玩家们都嫌太贵!曝英伟达考虑将RTX 4080降价
世界讯息:AI生成裸照谁之过?真相恐怕和你想的不一样
3小时超值!《阿凡达2》电影票价普遍低于50元 IMAX版80元
Redis 的 keys 命令你知道有多慢吗?
“抽烟哥”红到国外 田协紧急倡议应积极正向:全马冲进3个半小时是狠人
世界百事通!RTX 4080全球销售疲软:太贵了我再等等
《阿凡达2》伦敦首映式
惊险一幕!后车记录仪拍下特斯拉失控瞬间 网友分析司机被特斯拉辅助“救”两次
今日大雪:仲冬时节正式开始 全国大部气温回升雨雪稀少
每日观察!《原神》3.3版本今日上线:风元素新角色来了 还有原石可领
葡萄牙6-1大胜瑞士 C罗31场首发终结:加练千个西班牙点球大战仍出局 连续三届无缘8强
看齐QQ音乐?Apple Music新功能来了:支持iPhone、iPad唱卡拉OK
安兔兔11月iOS设备好评榜出炉:iPhone 14全系未上榜
SpringBoot构建RESTful风格应用
环球速讯:Kubernetes单机创建MySQL+Tomcat演示程序:《Kubernetes权威指南》第一章demo报错踩坑
全球最新:npm或者yarn安装sharp太慢、失败等问题
世界今亮点!不止是中药 连花清瘟新专利来了 可用于制作口罩、内衣、防护服等
时讯:200元耳机降噪效果比肩2000元!贝壳王子MO3 2代上手:同价位天花板
全国单日票房一度超4000万元:《阿凡达2》万众期待
热门:快升级5G!明年4G网速体验更糟糕:原因很无解
54年了!波音747飞机正式停产 一记录保持37年
焦点播报:MAUI新生3.4-深入理解XAML:数据模板DataTemplate
环球观热点:生成器函数
当前视点!Kubernetes configmap 笔记
画家要失业了?PS母公司Adobe开卖AI图片:侵权赔偿也自己扛
为什么海底火山不会被海水浇灭?
Javascript-极速入门指南-2-BOM与DOM操作-jQuery简介
54个CSS重难点整理,12-24篇,进阶高薪必需要掌握的知识点
国产CPU与国产OS联合 阿里平头哥加入openKylin社区
男子iPhone 13 Pro不到三个月自燃 法院:商品不符合质量要求 可以换新
全球热点!java创建线程的唯一方式
第一百一十篇:内存泄漏和垃圾回收(JS)
新资讯:《阿凡达2》获知名制作人小岛秀夫好评点赞:能够让人焕发激情
当前滚动:进军PC配件!一加将推出旗下首款机械键盘
《巫师3》次时代版“史诗升级”:官方Mod工具终于来了
AMD RX 7900又一非公卡亮相:档次上去了
每日热文:女子被绑浇墨汁?官方:自导自演 直播网红为赚流量博人眼球将严惩
left jon连接查询踩坑记
全球观热点:AMD RX 7900首批供货非常紧张!某品牌明年才能有
环球讯息:约4.1万人民币 法国一公司推出氢能电动自行车:像是助力车
环球焦点!一加11渲染图被网友恶搞:辨识度拉满