浅谈CompletableFuture

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

回顾FutureTask

之前我们已经学习过FutureTask以及线程池继承体系,里面介绍了线程池是如何借助FutureTask返回异步结果的:

而FutureTask#run()的作用就是执行任务,并把最终结果设置到FutureTask.outcome中:

FutureTask#get()为了能获取到最终结果,内部会阻塞,直到outcome被赋值:

基于以上原因,实际编程中如果期望得到异步结果,一般有两种方式:

  • FutureTask#get()阻塞等待
  • 判断FutureTask#isDone(),如果为true则返回

第一种大家都比较熟悉,下面演示第二种:

@RunWith(SpringRunner.class)
@SpringBootTest
public class CompletableFutureTest {private final ExecutorService executor = Executors.newFixedThreadPool(5);/*** 轮询异步结果并获取** @throws ExecutionException* @throws InterruptedException*/@Testpublic void testFutureAsk() throws ExecutionException, InterruptedException {// 任务1Future<String> runnableFuture = executor.submit(new Runnable() {@Overridepublic void run() {try {System.out.println("Runnable异步线程开始...");TimeUnit.SECONDS.sleep(3);System.out.println("Runnable异步线程结束...");} catch (InterruptedException e) {e.printStackTrace();}}}, "fakeRunnableResult");// 任务2Future<String> callableFuture = executor.submit(new Callable<String>() {@Overridepublic String call() throws Exception {System.out.println("Callable异步线程开始...");TimeUnit.SECONDS.sleep(3);System.out.println("Callable异步线程结束...");return "callableResult";}});boolean runnableDone = false;boolean callableDone = false;// 不断轮询,直到所有任务结束while (true) {TimeUnit.MILLISECONDS.sleep(500);System.out.println("轮询异步结果...");if (runnableFuture.isDone()) {System.out.println("Runnable执行结果:" + runnableFuture.get());runnableDone = true;}if (callableFuture.isDone()) {System.out.println("Callable执行结果:" + callableFuture.get());callableDone = true;}if (runnableDone && callableDone) {break;}}System.out.println("任务全部结束");}
}

结果

Runnable异步线程开始...

Callable异步线程开始...

轮询异步结果...

轮询异步结果...

轮询异步结果...

轮询异步结果...

轮询异步结果...

Runnable异步线程结束...

Callable异步线程结束...

轮询异步结果...

Runnable执行结果:fakeRunnableResult

Callable执行结果:callableResult

任务全部结束

FutureTask的不足

FutureTask其实各方面都比较完美,初见时甚至让人惊艳,因为它允许我们获取异步执行的结果!但FutureTask#get()本身是阻塞的,假设当前有三个下载任务在执行:

  • task1(预计耗时5秒)
  • task2(预计耗时1秒)
  • task3(预计耗时1秒)

如果阻塞获取时不凑巧把task1.get()排在最前面,那么会造成一定的资源浪费,因为task2和task3早就已经准备好了,可以先拿出来处理,以获得最佳的用户体验。

我们固然可以像上面的Demo一样,结合轮询+isDone()的方式改进,但仍存在以下问题:

  • 轮询间隔多少合适?
  • 为了避免while(true)阻塞主线程逻辑,可能需要开启单独的线程轮询,浪费一个线程
  • 仍然无法处理复杂的任务依赖关系

特别是第三点,使用FutureTask几乎难以编写...也就是说FutureTask很难处理异步编排问题。

CompletableFuture:基于异步回调的Future

CompletableFuture VS FutureTask

废话不多说,直接上代码:

@Test
public void testCallBack() throws InterruptedException, ExecutionException {// 提交一个任务,返回CompletableFutureCompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(new Supplier<String>() {@Overridepublic String get() {System.out.println("=============>异步线程开始...");System.out.println("=============>异步线程为:" + Thread.currentThread().getName());try {TimeUnit.SECONDS.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("=============>异步线程结束...");return "supplierResult";}});// 阻塞获取结果System.out.println("异步结果是:" + completableFuture.get());System.out.println("main结束");
}

结果

=============>异步线程开始...

=============>异步线程为:ForkJoinPool.commonPool-worker-9

=============>异步线程结束...

异步结果是:supplierResult

main结束

整个过程看起来和同步没啥区别,因为我们在main线程中使用了CompletableFuture#get(),直接阻塞了。当然你也可以使用CompletableFuture#isDone()改进,但我们并不推荐你把CompletableFuture当成FutureTask使用。

两者如此相似,有什么区别吗?

CompletableFuture和FutureTask的异同点:

  • 相同:都实现了Future接口,所以都可以使用诸如Future#get()、Future#isDone()、Future#cancel()等方法
  • 不同:
    • FutureTask实现了Runnable,所以它可以作为任务被执行,且内部维护outcome,可以存储结果
    • CompletableFuture没有实现Runnable,无法作为任务被执行,所以你无法把它直接丢给线程池执行,相反地,你可以把Supplier#get()这样的函数式接口实现类丢给它执行
    • CompletableFuture实现了CompletionStage,支持异步回调

总的来说,FutureTask和CompletableFuture最大的区别在于,FutureTask需要我们主动阻塞获取,而CompletableFuture支持异步回调(后面演示)。

如果大家拿上面的代码和之前的线程池+Runnable/Callable对比,就会发现CompletableFuture好像承担的其实是线程池的角色,而Supplier#get()则对应Runnable#run()、Callable#call()。但我们在分析线程池继承体系时从未见过CompletableFuture,Supplier也只是Java8预置的函数式接口而已,并不是任务类。

也就是说,不是线程池的CompletableFuture + 不是任务类的函数式接口实例,竟然把异步任务搞定了!

所以:

  • CompletableFuture底层到底做了什么?
  • 它为什么能把函数式接口的实例作为任务执行?明明既不是Runnable也不是Callable!
  • CompletionStage和异步回调之间有什么关系?

CompletableFuture与CompletionStage

大家可能对于CompletionStage比较陌生,没关系,先看代码:

@Test
public void testCallBack() throws InterruptedException, ExecutionException {// 提交一个任务,返回CompletableFuture(注意,并不是把CompletableFuture提交到线程池,它没有实现Runnable)CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(new Supplier<String>() {@Overridepublic String get() {System.out.println("=============>异步线程开始...");System.out.println("=============>异步线程为:" + Thread.currentThread().getName());try {TimeUnit.SECONDS.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("=============>异步线程结束...");return "supplierResult";}});// 异步回调:上面的Supplier#get()返回结果后,异步线程会回调BiConsumer#accept()completableFuture.whenComplete(new BiConsumer<String, Throwable>() {@Overridepublic void accept(String s, Throwable throwable) {System.out.println("=============>异步任务结束回调...");System.out.println("=============>回调线程为:" + Thread.currentThread().getName());}});// CompletableFuture的异步线程是守护线程,一旦main结束就没了,为了看到打印结果,需要让main休眠一会儿System.out.println("main结束");TimeUnit.SECONDS.sleep(15);
}

结果

=============>异步线程开始...

=============>异步线程为:ForkJoinPool.commonPool-worker-9

main结束

=============>异步线程结束...

=============>异步任务结束回调...

=============>回调线程为:ForkJoinPool.commonPool-worker-9

你可以暂时把每一部分的方法理解为提交一个任务。

到这里,大家应该会有两个疑问:

  • CompletionStage是什么?
  • 它和异步回调有啥关系?

本小节先回答第一个问题,第二个问题留待后续章节阐述(见右侧目录<异步回调的实现机制>)。

主线程调用了CompletableFuture#whenComplete():

// 异步回调:上面的Supplier#get()返回结果后,异步线程会回调BiConsumer#accept()
completableFuture.whenComplete(new BiConsumer<String, Throwable>() {@Overridepublic void accept(String s, Throwable throwable) {System.out.println("=============>异步任务结束回调...");}
});

实际上这个方法定义在CompletionStage接口中(方法超级多):

public interface CompletionStage<T> {// 省略其他方法.../*** Returns a new CompletionStage with the same result or exception as* this stage, that executes the given action when this stage completes.** <p>When this stage is complete, the given action is invoked with the* result (or {@code null} if none) and the exception (or {@code null}* if none) of this stage as arguments.  The returned stage is completed* when the action returns.  If the supplied action itself encounters an* exception, then the returned stage exceptionally completes with this* exception unless this stage also completed exceptionally.** @param action the action to perform* @return the new CompletionStage*/public CompletionStage<T> whenComplete(BiConsumer<? super T, ? super Throwable> action);// 省略其他方法...
}

而CompletableFuture实现了whenComplete():

public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {// 省略其他方法...public CompletableFuture<T> whenComplete(BiConsumer<? super T, ? super Throwable> action) {return uniWhenCompleteStage(null, action);}private CompletableFuture<T> uniWhenCompleteStage(Executor e, BiConsumer<? super T, ? super Throwable> f) {if (f == null) throw new NullPointerException();CompletableFuture<T> d = new CompletableFuture<T>();if (e != null || !d.uniWhenComplete(this, f, null)) {UniWhenComplete<T> c = new UniWhenComplete<T>(e, d, this, f);push(c);c.tryFire(SYNC);}return d;}// 省略其他方法...
}

所以,CompletionStage是什么呢?

我的回答是:

  • 它是一个“很简单”的接口。完全独立,没有继承任何其他接口,所有方法都是它自己定义的。
public interface CompletionStage<T> {// 定义了超级多类似whenComplete()的方法
}
  • 它是个不简单的接口。因为CompletableFuture实现Future的同时,还实现了它。Future方法就6、7个,而CompletionStage的方法超级多,所以如果你打开CompletableFuture的源码,目之所及几乎都是它对CompletionStage的实现。
public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {// 一些字段// 实现Future的方法// 实现CompletionStage的方法// 一些私有方法,配合CompletionStage// 一些内部类,配合CompletionStage
}
  • 异步回调其实和CompletionStage有着很大的关系(废话,FutureTask也实现了Future,但不能异步回调)

总而言之,CompletionStage是一个接口,定义了一些方法,CompletableFuture实现了这些方法并设计出了异步回调的机制。

具体怎么做到的,稍后见分晓。

一个小细节

有一个细节,很多人应该都不曾注意:

上面注释说的是:

异步线程会回调BiConsumer#accept()

咋一看没什么神奇,但你如果停下来理一下自己的思路,就会发现刚才你以为的是:

异步线程会回调CompletableFuture#whenComplete()

上面介绍CompletionStage时,我说的是“主线程调用了CompletableFuture#whenComplete()”,而不是异步线程调用。

换句话说,CompletionStage中定义的诸如whenComplete()等方法虽然和异步回调有关系,但并不是最终被回调的方法,最终被回调的其实是whenComplete(BiConsumer)传进去的BiConsumer#accept()。

到这里,你可能懵逼了,那这个CompletionStage定义了那么多方法,到底是干啥用的?异步线程又为何会回调它传入的函数接口的方法呢,BiConsumer#accept()明明不是Runnable#run()、Callable#call()呀!

异步回调的实现机制

上面种种表述,似乎都在暗示CompletableFuture#supplyAsync()会开启一个异步线程,然后才有后面的一系列异步回调操作。

为了更好地理解CompletableFuture的异步回调,我们对现有的疑问进行进一步的拆分:

左边为主线程,右边为异步线程。

异步线程哪来的,Supplier如何被执行?

之前分析过CompletableFuture和FutureTask的异同点,其中有一点提到:

  • CompletableFuture没有实现Runnable,无法作为任务被执行,所以你无法把它直接丢给线程池执行,相反地,你可以把Supplier#get()这样的函数式接口实现类丢给它执行

那么CompletableFuture为什么能执行“任务”,异步线程又是哪来的,为什么Supplier没有实现Runnable/Callable也能被执行?

跟随主线程进入CompletableFuture#supplyAsync(),我们会发现:

注意看Doug Lea大佬写的注释:

返回一个新的CompletableFuture,该future是由运行在{@link ForkJoinPool#commonPool()}中的任务异步完成的,其值是通过调用给定的Supplier获得的。

总的来说,和FutureTask有点像,都是传入某种参数,然后返回Future。

从Doug Lea大佬的注释中,我们可以窥见部分重要的信息:

  • 异步线程来自ForkJoinPool线程池
  • 通过CompletableFuture#supplyAsync(supplier)传入Supplier,返回CompletableFuture对象,它包含一个未来的value,且这个value会在稍后由异步线程执行Supplier#get()产生

如何验证大佬说的是不是正确的呢?

开个玩笑,后端之事,哪轮得到前端插嘴。

来,我们一起看源码~

我们可以看到CompletableFuture#supplyAsync(supplier)内部调用了asyncSupplyStage(asyncPool, supplier),此时传入了一个线程池asyncPool,它是CompletableFuture的成员变量:

useCommonPool为true时会使用ForkJoinPool,而useCommonPool取决于运行当前程序的硬件是否支持多核CPU,具体大家可以自己看源码。

现在我们已经确定了异步线程来自ForkJoinPool,剩下的问题是,主线程传进来的Supplier压根没有实现Runnable/Callable接口,怎么被异步线程执行呢?

哦~和ExecutorService#submit()一样的套路:包装成Task再执行。只不过这次被包装成了AsyncSupply,而不是FutureTask:

AsyncSupply名字虽然怪异,但和当初的FutureTask颇为相似,都实现了Future和Runnable,具备 任务+结果 双重属性:

然后就是熟悉的配方:

等线程池分配出线程,最终会执行AsyncSupply#run():

异步线程会执行AsyncSupply#run()并在方法内调用f.get(),也就是Supplier#get(),阻塞获取结果并通过d.completeValue(v)把值设置到CompletableFuture中,而CompletableFuture d已经在上一步asyncSupplyStage()中被返回。最终效果和线程池+FutureTask是一样的,先返回Future实例,再通过引用把值放进去。

所以,completableFuture.get()可以阻塞得到result:

至此,我们搞明白了异步线程是怎么来的,以及Supplier是如何被执行的。

从这个层面上来看,CompletableFuture相当于一个自带线程池的Future,而CompletableFuture#supplyAsync(Supplier)倒像是ExecutorService#submit(Runnable/Callable),内部也会包装任务,最终丢给Executor#execute(Task)。只不过ExecutorService是把Runnable#run()/Callable#call()包装成FutureTask,而CompletableFuture则把乱七八糟的Supplier#get()等函数式接口的方法包装成ForkJoinTask。

异步回调的原理

阻塞get()已经不足为奇,关键是回调机制如何实现?

在介绍CompletableFuture的回调机制之前,先跟大家说明一下,回调并没有大家想的那么神奇,尤其CompletableFuture的回调机制,其实本质上是对多个CompletableFuture内部函数的顺序执行,只不过发起者是异步线程而不是主线程:

现在第1个问题已经解决:

第2、3两个问题其实是同一个问题,放在一起讲。

为了能更好地说明问题,我们把原本第二部分的CompletableFuture#whenComplete()换成CompletableFuture#thenApply(),本质是一样的,顺便熟悉熟悉其他方法(也是CompletableFuture对CompletionStage的实现):

@RunWith(SpringRunner.class)
@SpringBootTest
public class CompletableFutureTest {@Testpublic void testCallBack() throws InterruptedException {// 任务一:把第一个任务推进去,顺便开启异步线程CompletableFuture<String> completableFuture1 = CompletableFuture.supplyAsync(new Supplier<String>() {@Overridepublic String get() {System.out.println("=============>异步线程开始...");try {TimeUnit.SECONDS.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("=============>completableFuture1任务结束...");System.out.println("=============>执行completableFuture1的线程为:" + Thread.currentThread().getName());return "supplierResult";}});System.out.println("completableFuture1:" + completableFuture1);// 任务二:把第二个任务推进去,等待异步回调CompletableFuture<String> completableFuture2 = completableFuture1.thenApply(new Function<String, String>() {@Overridepublic String apply(String s) {try {TimeUnit.SECONDS.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("=============>completableFuture2任务结束 result=" + s);System.out.println("=============>执行completableFuture2的线程为:" + Thread.currentThread().getName());return s;}});System.out.println("completableFuture2:" + completableFuture2);// 任务三:把第三个任务推进去,等待异步回调CompletableFuture<String> completableFuture3 = completableFuture2.thenApply(new Function<String, String>() {@Overridepublic String apply(String s) {try {TimeUnit.SECONDS.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("=============>completableFuture3任务结束 result=" + s);System.out.println("=============>执行completableFuture3的线程为:" + Thread.currentThread().getName());return s;}});System.out.println("completableFuture3:" + completableFuture3);System.out.println("主线程结束");TimeUnit.SECONDS.sleep(40);}
}

结果

completableFuture1:java.util.concurrent.CompletableFuture@76e4212[Not completed]

=============>异步线程开始...

completableFuture2:java.util.concurrent.CompletableFuture@23121d14[Not completed]

completableFuture3:java.util.concurrent.CompletableFuture@72af90e8[Not completed]

主线程结束

=============>completableFuture1任务结束...

=============>执行completableFuture1的线程为:ForkJoinPool.commonPool-worker-9

=============>completableFuture2任务结束 result=supplierResult

=============>执行completableFuture2的线程为:ForkJoinPool.commonPool-worker-9

=============>completableFuture3任务结束 result=supplierResult

=============>执行completableFuture3的线程为:ForkJoinPool.commonPool-worker-9

分析主线程的主干:

  • CompletableFuture#supplyAsync(Supplier):包装Supplier为AsyncSupply,调用executor.execute(),等待异步线程回调Supplier#get()
  • CompletableFuture#thenApply(Function)
  • CompletableFuture#thenApply(Function)

在之前介绍CompletionStage时,我很对不住这位老哥,隆重介绍了一番,结果发现CompletionStage#whenComplete()、CompletionStage#thenApply()竟然是主线程调用的,而不是异步回调。那么,主线程调用whenComplete(BiConsumer)、thenApply(Function)时到底做了什么,导致异步线程最终会执行BiConsumer#accept()、Function#apply()呢?

请大家把上面代码中的休眠时间统一改为100秒,方便留出分析时间。

然后再跟着我打几个断点:

点进CompletableFuture#thenApply(Function):

OK,DEBUG模式启动测试案例,多体会几遍Main的执行流程,必要时停止程序熟悉一下代码。

五分钟后...

应该已经走过好几遍了吧?让我们一起来看看。

相信大家对于uniApply()印象很深,因为对于主线程而言这个方法几乎相当于没有执行,每次都返回了false。

CompletableFutureTest目前有三块代码:任务一、任务二、任务三。

主线程在执行“任务一”的CompletableFuture#supplyAsync(Supplier)时,将Supplier包装成AsyncSupply任务,并开启了异步线程,此后异步线程会阻塞在Supplier#get():

也就是说,Supplier#get()是异步线程开启后执行的第一站!

与此同时,主线程继续执行后面的“任务二”、“任务三”,并且都会到达uniApply(),且都返回false,因为a.result==null。

以“任务二”为例,当主线程从任务二进来,调用thenApply():

最终会到达uniApply(),通过控制台的日志,我们发现a其实就是completableFuture1:

因为uniApply()的上一步传入的this:

也就是说:

主线程 ---> completableFuture1.thenApply(Function#apply) ---> !d.uniApply(this, f#apply, null)

a.result就是completableFuture1.result,而completableFuture1的值来自Supplier#get(),此时确实还是null(异步线程阻塞100秒后才会)。

所以此时d.uniApply(this, f, null) 为false,那么!d.uniApply(this, f, null) 为true,就会进入if语句:

主要做了3件事:

  • 传入Executor e、新建的CompletableFuture d、当前completableFuture1、Function f,构建UniApply
  • push(uniApply)
  • uniApply.tryFire(SYNC)

任务一做了两件事:

  • 开启异步线程
  • 等待回调

由于要开启线程,自己也要作为任务被执行,所以Supplier#get()被包装成AsyncSupply,是一个Task。而后续的几个任务其实只做了一件事:等待回调。只要能通过实例执行方法即可,和任务一有所不同,所以只是被包装成UniApply对象。

push(uniApply)姑且认为是把任务二的Function#apply()包装后塞到任务栈中。

但uniApply.tryFire(SYNC)是干嘛的呢?里面又调了一次uniApply():

SYNC=0,所以最终判断!d.uniApply(this, f, this) ==true,tryFire(SYNC)返回null,后面的d.postFire(a, mode)此时并不会执行,等后面异步线程复苏后,带着任务一的结果再次调用时,效果就截然不同了。

总结一下,“任务二”、“任务三”操作都是一样的,都做了3件事:

  • 主线程调用CompletableFuture#thenApply(Function f)传入f,构建UniApply对象,包装Function#apply()
  • 把构建好的UniApply对象push到栈中
  • 返回CompletableFuture d

绿色的是异步线程,此时阻塞等待Supplier#get(),但主线程没闲着,正在努力构建任务栈。

等过了100秒,supplyAsync(Supplier)中的Supplier#get()返回结果后,异步线程继续往下走:

看到了吗,postComplete()也会走uniApply(),但这次已经有了异步结果result,所以流程不会被截断,最终会调用Function#apply(s),而这个s是上一个函数的执行结果。也就是说,新的CompletableFuture对象调用Function#apply()处理了上一个CompletableFuture产生的结果。

最后,为CompletionStage老哥扳回颜面,你是最棒的:

CompletableFuture#whenComplete(BiConsumer)、CompletableFuture#thenApply(Function)等方法的目的是把BiConsumer#accept()及Function#apply()等回调函数封装成一个个UniApply对象被压入栈,等异步线程执行时,再逐个弹栈并回调。

黑线先行,绿色的异步线程阻塞一会后再走,此时主线程已经成功构建任务栈,引导异步线程去执行即可。

所以,总的来说CompletionStage设计得很巧妙,Doug Lea老爷子不愧是独立设计了JUC的男人,数据结构功底和对编程的理解举世无双。

当然,CompletableFuture还有其他很多的API,甚至回调任务过程中还可以再开异步线程,本文只分析了supplyAsync()+thenApply(),但原理大致相同。老实说,内部的实现机制比较复杂,个人不建议继续深入研究源码,意义不大。

CompletableFuture与FutureTask线程数对比

CompletableFuture和FutureTask耗费的线程数是一致的,但对于FutureTask来说,无论是轮询还是阻塞get,都会导致主线程无法继续其他任务,又或者主线程可以继续其他任务,但要时不时check FutureTask是否已经完成任务,比较糟心。而CompletableFuture则会根据我们编排的顺序逐个回调,是按照既定路线执行的。

其实无论是哪种方式,异步线程其实都需要阻塞等待结果,期间不能处理其他任务。但对于FutureTask而言,在异步线程注定无法复用的前提下,如果想要获取最终结果,需要主线程主动查询或者额外开启一个线程查询,并且可能造成阻塞,而CompletableFuture的异步任务执行、任务结果获取都是异步线程独立完成。

所以:

1个异步线程阻塞执行任务 + 回调异步结果 > 1个异步线程阻塞执行任务 + 1个线程阻塞查询任务

问题

鉴于篇幅较长,内容较深,所以设置一些问题,强制大家去思考整理吧。

假设有以下代码:

public class CompletableFutureTest {@Testpublic void testCallBack() throws InterruptedException {// 任务一CompletableFuture<String> completableFuture1 = CompletableFuture.supplyAsync(new Supplier<String>() {@Overridepublic String get() {return "supplierResult";}});System.out.println("completableFuture1:" + completableFuture1);// 任务二CompletableFuture<String> completableFuture2 = completableFuture1.thenApply(new Function<String, String>() {@Overridepublic String apply(String s) {return s;}});System.out.println("completableFuture2:" + completableFuture2);// 任务三CompletableFuture<String> completableFuture3 = completableFuture2.thenApply(new Function<String, String>() {@Overridepublic String apply(String s) {return s;}});System.out.println("completableFuture3:" + completableFuture3);System.out.println("主线程结束");TimeUnit.SECONDS.sleep(40);}
}
  • 主线程在执行任务一和任务二、任务三分别做了什么操作?
  • 哪些方法是主线程执行的,哪些方法是异步线程执行的?
  • Function#apply(String s)被回调时,形参哪来的?
  • 返回值CompletableFuture和异步结果之间的对应关系是怎样的?
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

进群,大家一起学习,一起进步,一起对抗互联网寒冬

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/213476.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

安全高效 江西变电站成功应用国家电网无人机巡检

随着电力需求的迅速增长&#xff0c;电网的巡检、维护与保养变得越来越重要。为迎接这一挑战&#xff0c;江西供电公司的一座变电站成功引入了复亚智能国家电网无人机巡检系统&#xff0c;在提升巡检水平、开创新型巡检模式方面做出了重要尝试&#xff0c;为电网设备的高效巡检…

EDA 数字时钟

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、数字时钟是什么&#xff1f;二、EDA里面数码管的显示1.元件模型2.参考程序3. 实验仿真波形4.实验现象5. 仿真问题 三、显示时钟1. 时钟电路模块2.参考程序3…

gRPC .net学习

学习helloworld server用.net client有.net的控制台 和 unity server端 直接使用vs2022创建(需自行看有无装asp.net哦),搜索gPRC,使用6.0吧&#xff0c;创建工程后直接F5跑起来,服务端到此完成 .net控制台client,创建新的控制台,使用NuGet,然后导入server端的Protos文件夹 学…

基于Jedis来探讨池化技术

为什么需要池化技术 系统运行时必然是需要数据库连接、线程等一些重量级对象&#xff0c;频繁的创建这种对象对性能有着不小的开销&#xff0c;所以为了减少没必要的创建和开销&#xff0c;我们就用到了池化技术。 通过创建一个资源池来保存这些资源便于后续的复用&#xff0c…

【C++初阶】七、内存管理(C/C++内存分布、C++内存管理方式、operator new / delete 函数、定位new表达式)

相关代码gitee自取&#xff1a; C语言学习日记: 加油努力 (gitee.com) 接上期&#xff1a; 【C初阶】六、类和对象&#xff08;初始化列表、static成员、友元、内部类&#xff09;-CSDN博客 目录 一 . C/C内存分布 C/C中程序内存区域划分&#xff1a; 二 . C内存管理方式 …

16.Java程序设计-基于SSM框架的android餐厅在线点单系统App设计与实现

摘要&#xff1a; 本研究旨在设计并实现一款基于SSM框架的Android餐厅在线点单系统&#xff0c;致力于提升餐厅点餐流程的效率和用户体验。通过整合Android移动应用和SSM框架的优势&#xff0c;该系统涵盖了用户管理、菜单浏览与点单、订单管理、支付与结算等多个功能模块&…

用户登录权限

文章目录 [TOC](文章目录) 前言一、 Cookie与session1.HTTP无状态2.cookie 和 session 的生命周期2.1 cookie 生命周期影响因素2.2 session 生命周期影响因素 3.cookie 和 session 的区别4.工作原理3 用户登录Node.js和Express验证session 二、JSON Web Token1. JWT 介绍2. JWT…

C#使用Matrix类对Dicom图像的放缩

C#使用Matrix类对Dicom图像的放缩&#xff0c;使用Matrix 1.同时操作水平、垂直同时放缩 // 创建一个 Matrix 对象 Matrix m_Matrix new Matrix();//放缩参数 float inputZoom1.2f; m_Matrix.Scale(inputZoom, inputZoom, MatrixOrder.Append); 2.操作水平&#xff08;X轴…

前端使用插件预览pdf、docx、xlsx、pptx格式文件

PDF预览 H5页面pdf预览 插件&#xff1a;pdfh5 版本&#xff1a;“pdfh5”: “^1.4.7” npm install pdfh5 import PdfH5 from "pdfh5"; import "pdfh5/css/pdfh5.css";// methods this.$nextTick(() > {this.pdfH5 new PdfH5("#pdf", {pd…

【算法系列篇】递归、搜索和回溯(二)

文章目录 前言1. 两两交换链表中的节点1.1 题目要求1.2 做题思路1.3 代码实现 2. Pow(X,N)2.1 题目要求2.2 做题思路2.3 代码实现 3. 计算布尔二叉树的值3.1 题目要求3.2 做题思路3.3 代码实现 4. 求根节点到叶结点数字之和4.1 题目要求4.2 做题思路4.3 代码实现 前言 前面为大…

计算机毕业设计springboot+ssm停车场车位预约系统java

管理员不可以注册账号 停车位包括车位所在楼层、车位编号、车位类型(全时间开放/高峰期开放)、预定状态等 用户预约时要求支付预约时间段的停车费用 违规行为&#xff1a;1.停车超过预约时间段 2.预约未使用 于系统的基本要求 &#xff08;1&#xff09;功能要求&am…

6G来袭,真的有必要吗?

6G来袭&#xff0c;6G标准将在2025年完成制定&#xff0c;2030年商用。当5G都还没玩明白的时候&#xff0c;6G又来了。 这次6G又提出了三个全新高大上场景&#xff0c;感知通信、人工智能通信、天地一体泛在物联&#xff0c;精英们还说&#xff0c;未来要连接很多机器人、元宇宙…

PHP基础 - 循环与条件语句

循环语句 1)for循环: 重复执行一个代码块指定的次数。 for ($i = 0; $i < 5; $i++) { // 初始化 $i 为 0,每次循环后将 $i 值增加 1,当 $i 小于 5 时执行循环echo "The number is: $i \n"; // 输出当前 $i 的值并换行 }// 循环输出结果为: // The number …

mysql字段设计规范:使用unsigned(无符号的)存储非负值

如果一个字段存储的是数值&#xff0c;并且是非负数&#xff0c;要设置为unsigned&#xff08;无符号的&#xff09;。 例如&#xff1a; 备注&#xff1a;对于类型是 FLOAT、 DOUBLE和 DECIMAL的&#xff0c;UNSIGNED属性已经废弃了&#xff0c;可能在mysql的未来某个版本去…

mysql分别在windows和linux下的备份策略

嗟乎&#xff01; 一、概述 mysql数据库该怎么备份呢&#xff1f; 数据库备份有几个概念&#xff1a;全量备份、增量备份、差异备份。当然啦&#xff0c;数据库备份又有冷备份和热备份&#xff0c;即物理备份和逻辑备份之分。冷备份就是将mysql停了&#xff0c;然后直接拷贝…

Python入门第2篇

pip包管理器 包管理器类似.NET下的nuget&#xff0c;主要用于管理引用依赖项。 安装Python的时候&#xff0c;已经默认安装了pip包管理器&#xff0c;因此无需单独安装 cmd&#xff0c;输入&#xff1a;pip --version 显示pip版本号信息&#xff0c;即代表pip安装成功&…

前端知识笔记(四十二)———http和https详细解析

HTTP&#xff08;Hypertext Transfer Protocol&#xff09;是一种用于在计算机网络中传输超文本的协议。它是一个客户端-服务器协议&#xff0c;用于从 Web 服务器传输超文本到本地浏览器。HTTP 使用 TCP/IP 协议作为底层传输协议&#xff0c;并使用默认端口号80。 HTTPS&…

8-tornado中模板的使用(通过字符串返回、通过模板Template返回、通过模板render返回)、模板案例

1 Template 1.1 通过字符串返回 import tornado class IndexHandler(web.RequestHandler):def get(self):arg Templateself.finish(f<h1>Hello {arg}!!</h1>)1.2 通过模板Template返回 tornado.template 一个简单的模板系统&#xff0c;将模板编译为Python代码。…

c 一,二,三维数组的定义和赋值

1. 定义数组必须指定数组的大小&#xff0c;也就是用多少存储空间来存储此数组 2.定义数组必须用数组的标准格式定义&#xff1a;数组名下标的形式 3.只有字符串可以用指针来定义 4.可以把c 中一切数和struct 理解为char 数组 比如int 就是4字节的char数组 #include <…

编程语言的演进历程与未来发展趋势

第一代 编程语言的发展历程起源于早期的机器语言阶段&#xff0c;这是一种由二进制代码构成的计算机能够直接解读并执行的语言。然而&#xff0c;鉴于其过于复杂且难以理解&#xff0c;故这一时代的语言并不常为人类所采纳。 第二代 紧接着产生的第二代语言旨在简化编程过程…