4次迭代,让我的 Client 优化 100倍!泄漏一个 人人可用的极品方案!

news2024/11/12 12:02:56

4次迭代,让我的HttpClient提速100倍

在大家的生产项目中,经常需要通过Client组件(HttpClient/OkHttp/JDK Connection)调用第三方接口。

尼恩的一个生产项目也不例外。

在一个高并发的中台生产项目中。有一个比较特殊的请求,一次请求,包含 10个 Web 外部服务调用,每个服务调用的预定时间200ms。

为了并行,使用了一个很大的线程池,最大100个线程。

注意,这里100个线程已经很多了,因为其他的环节也需要很多线程。所以不能太多了。

尼恩和团队小伙伴经过4次迭代,把这个客户端调用不断提升,最终的提升了100倍

为啥写此文呢?

尼恩最近在指导小伙伴的简历,发现很多小伙伴没有素材,简历写得太low、太low。在尼恩读者50+交流群中,尼恩经常指导小伙伴改简历。 改简历所涉及的一个要点是:

在 XXX 项目中,完成了 XXX 模块的代码优化

另外,在面试的过程中,面试官也常常喜欢针对提问,来考察候选人对代码质量的追求、对设计模式的应用能力:

你做过哪些代码优化?

大家一般的套路,都是通过模板模式、策略模式等,完成 XXXXX 模块的重构,提升代码的可扩展行,可维护性。

如果有类似扩展场景、类似优化经历的小伙伴还好。

头疼的是,很多小伙伴确实没有。然后无奈的说,没有做过代码的优化。

所以,为了能帮到大家,上一次给大家提供了一个人人可用的、涉及到切面思想、非常高明的的代码优化点:

阿里一面:你做过哪些代码优化?来一个人人可以用的极品案例

这一次又是一个大大的代码优化、性能优化点,甚至比上次的更为出彩。固,尼恩通过这个文章,把Client性能提升100倍的迭代过程记录下来,再一次为大家提供一个代码优化的参考答案

大家可以对着实操一下,然后写入简历,为简历增加一个不小的亮点。当然,如果确实不知道怎么优化简历,可以来找40岁老架构师尼恩

接下来看看,尼恩团队是怎么做到的。

注:本文以 PDF 持续更新,最新尼恩 架构笔记、面试题 的PDF文件,请从这里获取:码云

第1次迭代:使用 CompletableFuture 完成并发

最早的代码,是通过向线程池中提交任务的方式完成的,从代码的优雅程度上来说,比使用CompletableFuture 的方式,差得太远。

所以,第1轮迭代,是通过CompletableFuture进行代码的优化。这个环节,对性能没有本质的提升,但是CompletableFuture 这种 函数式编程的风格,为后面的迭代打下来一些技术基础。

使用CompletableFuture 并发10个远程api 接口,有两个方法

  • 方法1:CompletableFuture 和 CountDownLatch
  • 方法2:CompletableFuture.allOf 算子

在代码实现的时候,使用了结合使用CompletableFuture 和 CountDownLatch 进行并发回调

为了大家能体验效果,给大家提供了一段原始代码的参考程序,具体如下:

package com.crazymaker.springcloud;

import com.crazymaker.springcloud.common.util.ThreadUtil;
import org.junit.Test;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;

public class CoCurrentDemo {

    /**
     * 使用CompletableFuture 和  CountDownLatch  进行并发回调
     */

    @Test
    public void testMutiCallBack() {
        CountDownLatch countDownLatch = new CountDownLatch(10);
        //批量异步
        ExecutorService executor = ThreadUtil.getIoIntenseTargetThreadPool();
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10; i++) {
            CompletableFuture<Long> future = CompletableFuture.supplyAsync(() -> {
                long tid = ThreadUtil.getCurThreadId();
                 try {
                     doRpc(tid); //异步执行 远程调用
                 } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return tid;
            }, executor);

            future.thenAccept((tid) -> {
                System.out.println("线程" + tid + "结束了");
                countDownLatch.countDown();
            });
        }
        try {
            countDownLatch.await();
            //输出统计结果
            float time = System.currentTimeMillis() - start;

            System.out.println("所有任务已经执行完毕");
            System.out.println("运行的时长为(ms):" + time);

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private void doRpc(long tid) throws InterruptedException {
        System.out.println("线程" + tid + "开始了,模拟一下远程调用");
        Thread.sleep(200);
    }
}

尼恩提示:CompletableFuture 的知识,非常重要。具体请大家去阅读尼恩在清华大学出版社出版的《Java 高并发核心编程 卷2 加强版》。

很多小伙伴和尼恩说:这本书写得太好,之前读博客的时候,很多高并发的知识看不懂的,通过卷2加强版,都看懂了。

第2次迭代:响应式 Flux 流进行异步回调改造

回顾一下咱们这个优化的案例:一次请求,触发 10 个 Web 服务调用。

什么分析一下,发现一个秘密:执行一次WEB调用操作所需的实际工作非常少。实际的工作就2点:

  • 生成并触发一个请求
  • 等等一段时间,会收到一个响应。

而且,其中有一个节非常低性能,这个环节是:在请求和响应之间,线程根本没有做任何工作,而且在等待,等待200ms。

线程资源是宝贵的。咱们一个java应用不能开始太多的线程,一般最多就400个左右。如果开启太多,很多的cpu资源就用去做线程上下文切换了。

如何要让这些线程不等待,去干别的工作,该当如何?

这是使用异步框架。使用异步框架发出 Web 请求的核心优势之一,在请求进行中时您不会占用任何线程。第2次迭代:响应式 Flux 流进行异步回调改造。

使用异步框架的核心逻辑,如下所以:

Flux.range(0,100)
.subscribeOn(Schedulers.boundedElastic())
.flatMap(i -> webClientCallApi(i))
.subscribe(...) //定义结果

响应式的本质,是回调。其原理,具体请参考 尼恩的深度文章:《京东一面:20种异步,你知道几种? 含协程》

响应式编程门槛非常高,学习曲线很长。但是一旦掌握了其编程的风格之后,其实也就简单了。

有关响应式编程的内容,尼恩给大家准备了一本10W字的《响应式 圣经,pdf可以找尼恩获取》

在响应式框架中,我们的线程不用再死耗了,底层的逻辑是:把异步任务加入响应式组件的内部的队列之后,就去干起别的事情了,不需要去死死等200ms。

so,我们进行了第2次迭代:响应式Flux流进行异步回调改造。

为了大家能体验效果,给大家提供了一段原始代码的参考程序,具体如下:

private Flux<Integer> doRpcFlux(int key) {
    return Flux.create(sink -> {
        log.info("模拟远程调用,参数为:" + key);
        ThreadUtil.sleepMilliSeconds(100);
        sink.next(key);
        sink.complete();
    });
}

@Test
public void mergeTest5() throws InterruptedException {
    long startTime = LocalTiker.SINGLETON.now();
    Iterable<Integer> targetKeys = Arrays.asList( 101, 102, 103, 104, 105);
    Flux.fromIterable(targetKeys)
        .publishOn(Schedulers.fromExecutorService(
            ThreadUtil.getMixedTargetThreadPool()))
        .flatMap(key -> doRpcFlux(key))
        .doOnError(ReactorPrallelDemo::doOnError)
        .doOnComplete(ReactorPrallelDemo::doOnComplete)
        .doFinally(signalType -> log.info("并发执行的时间: " + LocalTiker.SINGLETON.gapFrom(startTime)))
        .subscribe(responseData -> log.info(responseData.toString()), e -> log.info("error:" + e.getMessage()));
    Thread.sleep(200000);
}    

第3次迭代:响应式 parallel Flux 流进行并行异步回调改造

在刚开始使用响应式编程的时候,感觉也没有耗费太长时间,就把传统的命令式代码,切换到了响应式的风格,不免洋洋得意。

第2次迭代响应式Flux流进行异步回调改造,以为十全十美,改造成功了。

实际不然,改造之后,虽然线程资源得到了有效的利用。但是由于前期对flux流的机制,不是太清楚。实际上的情况是,性能反而下降了。

实际上,flux的内部机制:flux流里边的任务,默认是串行执行的。

所以上面的代码,实际上是让10次调用 串行执行。初心是并行,实际上适得其反,变成了串行,性能反而大大下降。

怎么回到并行呢?需要使用 Flux parallel流。

下面是一段 使用paralle Flux 的demo:

Flux.range(0,100)
.parallel()
.runOn(Schedulers.boundedElastic())
.flatMap(i -> webClientCallApi(i))
.sequential().collecttoList();

so,我们进行了第3次迭代:响应式 parallel Flux 流进行并行异步回调改造。

为了大家能体验效果,给大家提供了一段原始代码的参考程序,具体如下:

@Test
public void mergeTest6() throws InterruptedException {
    long startTime = LocalTiker.SINGLETON.now();
    Iterable<Integer> targetKeys = Arrays.asList(100, 101, 102, 103, 104, 105, 106, 107);
    Flux.fromIterable(targetKeys)
        .parallel()
        .runOn(Schedulers.fromExecutorService(ThreadUtil.getMixedTargetThreadPool()))
        .flatMap(key -> doRpcFlux(key))
        .sequential()
        .doOnError(ReactorPrallelDemo::doOnError)
        .doOnComplete(ReactorPrallelDemo::doOnComplete)
        .doFinally(signalType -> log.info("并发执行的时间: " + LocalTiker.SINGLETON.gapFrom(startTime)))
        .subscribe(i -> log.info("->" + i));

    Thread.sleep(200000);
}

private static void doOnComplete() {
    log.info("并发远程调用异常完成");
}

运行的结果如下:

12:31:34.138 [Biz-1-cpu-2] INFO com.crazymaker.springcloud.user.main.ReactorPrallelDemo - this taskB  start
12:31:34.138 [Biz-1-cpu-1] INFO com.crazymaker.springcloud.user.main.ReactorPrallelDemo - this taskA  start
12:31:34.227 [main] DEBUG reactor.util.Loggers$LoggerFactory - Using Slf4j logging framework
12:31:34.296 [Biz-2-mixed-18] INFO com.crazymaker.springcloud.user.main.ReactorPrallelDemo - 模拟远程调用,参数为:101
12:31:34.296 [Biz-2-mixed-22] INFO com.crazymaker.springcloud.user.main.ReactorPrallelDemo - 模拟远程调用,参数为:105
12:31:34.296 [Biz-2-mixed-23] INFO com.crazymaker.springcloud.user.main.ReactorPrallelDemo - 模拟远程调用,参数为:106
12:31:34.296 [Biz-2-mixed-24] INFO com.crazymaker.springcloud.user.main.ReactorPrallelDemo - 模拟远程调用,参数为:107
12:31:34.296 [Biz-2-mixed-20] INFO com.crazymaker.springcloud.user.main.ReactorPrallelDemo - 模拟远程调用,参数为:103
12:31:34.296 [Biz-2-mixed-21] INFO com.crazymaker.springcloud.user.main.ReactorPrallelDemo - 模拟远程调用,参数为:104
12:31:34.296 [Biz-2-mixed-17] INFO com.crazymaker.springcloud.user.main.ReactorPrallelDemo - 模拟远程调用,参数为:100
12:31:34.296 [Biz-2-mixed-19] INFO com.crazymaker.springcloud.user.main.ReactorPrallelDemo - 模拟远程调用,参数为:102
12:31:34.398 [Biz-2-mixed-20] INFO com.crazymaker.springcloud.user.main.ReactorPrallelDemo - 并发执行的结果->103
12:31:34.398 [Biz-2-mixed-20] INFO com.crazymaker.springcloud.user.main.ReactorPrallelDemo - 并发执行的结果->100
12:31:34.398 [Biz-2-mixed-20] INFO com.crazymaker.springcloud.user.main.ReactorPrallelDemo - 并发执行的结果->101
12:31:34.398 [Biz-2-mixed-20] INFO com.crazymaker.springcloud.user.main.ReactorPrallelDemo - 并发执行的结果->102
12:31:34.398 [Biz-2-mixed-20] INFO com.crazymaker.springcloud.user.main.ReactorPrallelDemo - 并发执行的结果->104
12:31:34.398 [Biz-2-mixed-20] INFO com.crazymaker.springcloud.user.main.ReactorPrallelDemo - 并发执行的结果->105
12:31:34.398 [Biz-2-mixed-20] INFO com.crazymaker.springcloud.user.main.ReactorPrallelDemo - 并发执行的结果->106
12:31:34.398 [Biz-2-mixed-20] INFO com.crazymaker.springcloud.user.main.ReactorPrallelDemo - 并发执行的结果->107
12:31:34.398 [Biz-2-mixed-20] INFO com.crazymaker.springcloud.user.main.ReactorPrallelDemo - 并发远程调用异常完成
12:31:34.398 [Biz-2-mixed-20] INFO com.crazymaker.springcloud.user.main.ReactorPrallelDemo - 并发执行的时间: 105

响应式Flux parallel流进行并行异步回调改造,看上去已经圆满了。

终于,尼恩和小伙伴实现了两个效果:

  • 并行
  • 没有线程阻塞等待(异步回调)

还有点问题没有解决,就是底层组件是httpclient,没有使用异步非阻塞的netty组件。虽然底层IO组件还是存在线程阻塞,只是这个下一次迭代再说。

至少从业务维度,第三次迭代之后,没有线程阻塞等待。

接下来,说说parallel Flux并行 流的使用场景和基础原理。因为尼恩的社群中,经常有这块的生产问题,被小伙伴抛出来。说明这个组件,在生产场景有很大的使用价值。

parallel Flux 流的使用场景

Flux 本质上是将通量元素划分为单独的轨道,元素之间是串行。

Flux parallel 模式,就是将通量元素划分为并行的轨道,才是真正并行的。

Flux parallel 并行流,在生产场景,经常用到。

下面是来自尼恩的社群50+群中的一个生产问题:

@架构师尼恩

假设我上游有一个元素,下游需要根据这个元素有多个Task处理,但是需要等多个Task处理完成后才能返回,如果我不用ConnectFlux的话 ,还有什么其他方法解决吗?

类似于 Mono.just(1).map(a-> { executeTaskA(a); executeTaskB(a); }).subscribe();

我这个executeTaskA 和 executeTaskB 是并行操作的,不存在依赖关系

以上这样的生产问题,都可以使用并行流。

parallel Flux 的底层原理

1. parallel FLux 分配上游元素过程是

parallel操作符执行过后,会生成一个新的flux 即parallelFLux。parallelFLux能够接受多个订阅者,他会从原始flux中请求perfetch个元素,将上游传递push过来的元素依次按顺序分配给子订阅者。

parallelFLux分配上游元素过程是:

依次分配给下游每个订阅者,当某个子订阅者请求数已经满足时,将跳过此订阅者继续向下一个子订阅者分配。

他有两个参数 parallelism 和 prefetch,

  • parallelism 第一个参数表示划分子流的数量,默认等于cpu核心数,
  • prefetch表示向原始流预取的数量。

parallel本质上是包装每个子订阅者,将接受到的数据存入子订阅者的queue中,然后使用线程池处理queue内的数据。

2. parallel 操作符后接 map/flapMap 操作符

map/flapMap操作符会将真实订阅者生成代理,他将上游onNext的值使用map/flapMap操作符进行转换。

在parallelFlux场景下,它会将每个子订阅者包装起来进行转换。

3. parallel 后面可以接 sequential 操作符

sequential 可以将parallelFlux转成普通flux,

他的原理是创建parallel需要的子订阅者,使用子订阅者订阅parallelFlux,然后将数据依次从每个子订阅者中获取到数据,向下游发送。

在sequential操作执行时,数据可能来自多个线程,这里采用的是哪个线程先到则负责将其他子订阅者队列中的值向下发,没获取到锁的线程存入队列直接退出。

sequential允许传递参数,表示每个子订阅者身上能存储的元素最大数。

4.parallel 后接 runOn 操作符

parallel本质上是包装每个子订阅者,将接受到的数据存入子订阅者的queue中,然后使用线程池处理queue内的数据。

runOn处理的逻辑也是和上面sequential类似,使用AtomicInteger限制每个子订阅者同时只会有一个线程在执行,如果当前已经存在一个线程在执行,后来的数据存入queue后会直接退出。

它还可以传递第二个参数表示每个子订阅者预取的数量,这里为什么要预取呢?

是因为如果下游订阅者请求了非常大的数量,而runOn将任务分配到线程池中本身是非堵塞的,也就是任务全部堆积在线程池中,可能导致内存溢出。所以使用预取参数分批次满足下游的请求数量。

第4次迭代:响应式 WebClient 组件去掉底层的io线程阻塞

对于客户端调用要提升100倍,没有那么容易。

路漫漫其修远,尼恩团队上下求索。

虽然httpclient也支持高性能的nio 模式,但是httpclient 线程模型是阻塞的,httpclient没有和Netty这样的reactor反应器线程模型。

httpclient底层的io线程是阻塞式的,具体如下图:

Netty这样的reactor反应器线程模型,底层的io线程是非阻塞式的

所以,要彻底提升性能,底层需要非阻塞。

如果底层的io传输,也需要使用反应器线程,怎么办? 3个措施:

  • 那么要么使用netty,
  • 要么使用基于netty的http组件。
  • 或者基于netty自己进行封组

这里,使用了响应式的http组件,WebClient组件。使用 WebClient 进行了第四次迭代。

为了大家能体验效果,给大家提供了一段原始代码的参考程序,具体如下:

@Test
public void mergeTest7() throws InterruptedException {
    long startTime = LocalTiker.SINGLETON.now();
    Iterable<Integer> targetKeys = Arrays.asList( 101, 102, 103, 104, 105);
    Flux.fromIterable(targetKeys)
        .parallel()
        .runOn(Schedulers.fromExecutorService(ThreadUtil.getMixedTargetThreadPool()))
        .flatMap(key -> doRpcUseWebClient(key))
        .sequential()
        .doOnError(ReactorPrallelDemo::doOnError)
        .doOnComplete(ReactorPrallelDemo::doOnComplete)
        .doFinally(signalType -> log.info("并发执行的时间: " + LocalTiker.SINGLETON.gapFrom(startTime)))
        .subscribe(responseData -> log.info(responseData.toString()), e -> log.info("error:" + e.getMessage()));
    Thread.sleep(200000);
}

private Flux<RestOut<JSONObject>> doRpcUseWebClient(int key) {

    //响应式客户端
    WebClient client = null;

    WebClient.RequestBodySpec request = null;

    //方式一:极简创建
    //方式二:使用builder创建
    client = WebClient.builder()
        .baseUrl("")
        .defaultHeader(HttpHeaders.CONTENT_TYPE, "application/json")
        .defaultHeader(HttpHeaders.USER_AGENT, "Spring 5 WebClient")
        .build();
    String restUrl = "http://crazy.api/demo/hello/v1?key=" + key;
    /**
     * 是通过 WebClient 组件构建请求
     */
    request = client
        // 请求方法
        .method(HttpMethod.GET)
        // 请求url 和 参数
        //                .uri(restUrl, params)
        .uri(restUrl)
        // 媒体的类型
        .accept(MediaType.APPLICATION_JSON);

    WebClient.ResponseSpec retrieve = request.retrieve();
    // 处理异常 请求发出去之后判断一下返回码
    retrieve.onStatus(status -> status.value() == 404,
                      response -> Mono.just(new RuntimeException("Not Found")));

    ParameterizedTypeReference<RestOut<JSONObject>> parameterizedTypeReference =
        new ParameterizedTypeReference<RestOut<JSONObject>>() {
    };
    // 返回流
    Mono<RestOut<JSONObject>> resp = retrieve.bodyToMono(parameterizedTypeReference);
    return Flux.from(resp);
}

终于,让我的 HttpClient 提速100倍

最终,通过测试的验证,实际效果提升了100多倍。

这里咱们从理论上计算一下,这里的4次迭代,为啥能够提升了100多倍?

假设一次请求的10个 Web 外部服务调用,每个服务调用的预定时间200ms,使用了100个线程的线程池

那么:100个线程,同时可以并发完成10个请求,1s可以并发50个请求,吞吐量为 50qps

经过异步改造之后,线程没有任何阻塞,在网卡(千兆)限制范围内 + 和后端的吞吐量保证 下,可以轻松实现5000qps,甚至更多。

最终,性能轻松提速了100倍+。

说在最后

以上内容作为生产案例,收录于《响应式圣经 V2》供大家学习和参考,此书pdf电子版,可以找尼恩获取。

另外,很多小伙伴的小伙伴没有技术亮点,可以参考这个案例,做个实操,然后作为一个小亮点放到简历里边。如果不会刷入简历,可以来找尼恩做简历指导

在云原生时代、高并发时代,很多底层组件都开始响应式编程,所以响应式编程将会越来越流行

虽然说,响应式编程虽然很晦涩难懂,但是掌握了、习惯了后,其实也就那么回事,反而会享受响应式编程的优雅。

关于响应式编程的技术问题,可以来找尼恩交流。 最后,送大家一本不断迭代的电子书,帮助大家在响应式时代,游刃有余。

响应式圣经:10W字实现响应式编程自由 v2

推荐阅读:

《字节二面:10Wqps超高流量系统,如何设计?》

《全链路异步,让你的 SpringCloud 性能优化10倍+》

《Linux命令大全:2W多字,一次实现Linux自由》

《阿里一面:谈一下你对DDD的理解?2W字,帮你实现DDD自由》

《网易二面:CPU狂飙900%,该怎么处理?》

《阿里二面:千万级、亿级数据,如何性能优化? 教科书级 答案来了》

《峰值21WQps、亿级DAU,小游戏《羊了个羊》是怎么架构的?》

《场景题:假设10W人突访,你的系统如何做到不 雪崩?》

《2个大厂 100亿级 超大流量 红包 架构方案》

《Nginx面试题(史上最全 + 持续更新)》

《K8S面试题(史上最全 + 持续更新)》

《操作系统面试题(史上最全、持续更新)》

《Docker面试题(史上最全 + 持续更新)》

《Springcloud gateway 底层原理、核心实战 (史上最全)》

《Flux、Mono、Reactor 实战(史上最全)》

《sentinel (史上最全)》

《Nacos (史上最全)》

《TCP协议详解 (史上最全)》

《分库分表 Sharding-JDBC 底层原理、核心实战(史上最全)》

《clickhouse 超底层原理 + 高可用实操 (史上最全)》

《nacos高可用(图解+秒懂+史上最全)》

《队列之王: Disruptor 原理、架构、源码 一文穿透》

《环形队列、 条带环形队列 Striped-RingBuffer (史上最全)》

《一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之间混乱关系(史上最全)》

《单例模式(史上最全)》

《红黑树( 图解 + 秒懂 + 史上最全)》

《分布式事务 (秒懂)》

《缓存之王:Caffeine 源码、架构、原理(史上最全,10W字 超级长文)》

《缓存之王:Caffeine 的使用(史上最全)》

《Java Agent 探针、字节码增强 ByteBuddy(史上最全)》

《Docker原理(图解+秒懂+史上最全)》

《Redis分布式锁(图解 - 秒懂 - 史上最全)》

《Zookeeper 分布式锁 - 图解 - 秒懂》

《Zookeeper Curator 事件监听 - 10分钟看懂》

《Netty 粘包 拆包 | 史上最全解读》

《Netty 100万级高并发服务器配置》

《Springcloud 高并发 配置 (一文全懂)》

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

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

相关文章

「文档数据库之争」MongoDB和CouchDB的比较

MongoDB和CouchDB都是基于文档的NoSQL数据库类型。文档数据库又称mdocument store&#xff0c;通常用于存储半结构化数据的文档格式及其详细描述。它允许创建和更新程序&#xff0c;而不需要引用主模式。移动应用程序中的内容管理和数据处理是可以应用文档存储的两个字段。Mong…

积分球原理及积分球类型介绍

标题积分球标准型积分球LED积分球均匀光源便携式高亮度积分球均匀光源微光积分球均匀光源积分球均匀光源iSphere高光谱响应光学积分球其他分类积分球 积分球原理:由于球体内整涂有白色漫反射材料的空腔球体&#xff0c;球壁上开有采样口&#xff0c;当待测样品光源进入积分球的…

Java之关于String字符串笔试面试重点

目录 一.关于字符串的常量池 1.关于字符串产生的三种方式 2.关于字符串的常量池 3.直接赋值法和new的方式产生对象的区别 二.关于intern方法 1.情况一(已经包含) 2.情况二(已经包含) 3.情况三(未包含) 4.情况四 三.关于字符串的不可变性 1.了解字符串的不可变性 2.Str…

python+selenium使用webdriver启动chrome出现闪退现象解决

这两天发现之前开发的爬虫程序出问题了&#xff1a;谷歌浏览器出现打开立即闪退的现象&#xff0c;代码未修改过&#xff0c;检查也没有任何问题&#xff01; 查看chrome浏览器发现版本更新了 ↑&#xff08;点击chrome浏览器右上角三个点&#xff0c;最下面帮助→Google Chr…

[软件工程导论(第六版)]第9章 面向对象方法学引论(课后习题详解)

文章目录1. 什么是面向对象方法学&#xff1f;它有哪些优点&#xff1f;2. 什么是“对象”&#xff1f;它与传统的数据有何异同&#xff1f;3. 什么是“类”&#xff1f;4. 什么是“继承”&#xff1f;5. 什么是模型&#xff1f;开发软件为何要建模&#xff1f;6. 什么是对象模…

[软件工程导论(第六版)]第8章 维护(复习笔记)

文章目录8.1 软件维护的定义8.2 软件维护的特点8.3 软件维护过程8.4 软件的可维护性8.5 预防性维护8.6 软件再工程过程维护的基本任务&#xff1a;保证软件在一个相当长的时期能够正常运行软件工程的主要目的就是要提高软件的可维护性&#xff0c;减少软件维护所需要的工作量&a…

C语言编程规范 第一部分

、代码总体原则 1、清晰第一 清晰性是易于维护、易于重构的程序必需具备的特征。代码首先是给人读的&#xff0c;好的代码应当可以像文章一样发声朗诵出来。目前软件维护期成本占整个生命周期成本的40%~90%。根据业界经验&#xff0c;维护期变更代码的成本&#xff0c;小型系统…

Spring Batch ItemProcessor组件-默认处理器

目录 引言 概念 默认ItemProcessor ValidatingItemProcessor&#xff1a;校验处理器 ItemProcessorAdapter&#xff1a;适配器处理器 ScriptItemProcessor&#xff1a;脚本处理器 CompositeItemProcessor&#xff1a;组合处理器 自定义ItemProcessor处理器 转视频版 引…

图数据库中的 OLTP 与 OLAP 融合实践

在一些图计算的场景下&#xff0c;我们会遇到同时需要处理 OLTP 和 OLAP 的问题。而本文就给了一个 OLTP 与 OLAP 融合实践的指导思路&#xff0c;希望给你带来一点启发。 Dag Controller 介绍 Dag Controller 是 NebulaGraph 企业版的图系统&#xff0c;经过反复测试无误后已…

rabbitMq安装(小短文)--未完成

rabbitMq是在activeMq的基础上创造的&#xff0c;有前者的功能&#xff0c;比前者强&#xff0c;属于后来居上。系统环境:windows10首先下载相关软件Erlang&#xff0c;因为他是这个语言写的。https://www.erlang.org/downloads然后安装&#xff0c;并且弄到环境变量里验证是否…

Robot Framework自动化测试---元素定位

不要误认为Robot framework 只是个web UI测试工具&#xff0c;更正确的理解Robot framework是个测试框架&#xff0c;之所以可以拿来做web UI层的自动化是国为我们加入了selenium2的API。比如笔者所处工作中&#xff0c;更多的是拿Robot framework来做数据库的接口测试&#xf…

Leetcode.2257 统计网格图中没有被保卫的格子数

题目链接 Leetcode.2257 统计网格图中没有被保卫的格子数 Rating &#xff1a; 1709 题目描述 给你两个整数 m和 n表示一个下标从 0开始的 m x n网格图。同时给你两个二维整数数组 guards和 walls&#xff0c;其中 guards[i] [rowi, coli]且 walls[j] [rowj, colj]&#xff…

适合初学者的超详细实用调试技巧(下)

我们日常写代码的时候&#xff0c;常常会遇到bug的情况&#xff0c;这个时候像我这样的初学者就会像无头苍蝇一样这里改改那里删删&#xff0c;调试的重要性也就显现出来&#xff0c;这篇文章接着上文来讲解。 上文地址&#xff1a;(8条消息) 适合初学者的超详细实用调试技巧&…

Tomcat目录介绍,结构目录有哪些?哪些常用?

bin 启动&#xff0c;关闭和其他脚本。这些 .sh文件&#xff08;对于Unix系统&#xff09;是这些.bat文件的功能副本&#xff08;对于Windows系统&#xff09;。由于Win32命令行缺少某些功能&#xff0c;因此此处包含一些其他文件。 比如说&#xff1a;windows下启动tomcat用的…

3款强大到离谱的电脑软件,个个提效神器,从此远离加班

推荐3款让你偷懒&#xff0c;让你上头的提效电脑软件&#xff0c;个个功能强大&#xff0c;让你远离加班&#xff01; 很多几个小时才能做好的事情&#xff0c;用上它们&#xff0c;只需要5分钟就行&#xff01;&#xff01; 1、JNPF —— 个人最喜欢的低代码软件 它为开发者…

利用OpenCV的函数equalizeHist()对图像作直方图均衡化处理

如果一幅图像的灰度值集中在某个比较窄的区域&#xff0c;则图像的对比度会显得比较小&#xff0c;不便于对图像的分析和处理。 图像的直方图均衡化可以实现将原图像的灰度值范围扩大&#xff0c;这样图像的对比度就得到了提高&#xff0c;从而方便对图像进行后续的分析和处理…

从Docker挂载逃逸原理复现分析到BlueMoon实战

Docker逃逸 什么是Docker Docker 是一个开源的应用容器引擎&#xff0c;让开发者可以打包他们的应用以及依赖包到一个可移植的镜像中&#xff0c;然后发布到任何流行的 Linux或Windows操作系统的机器上&#xff0c;也可以实现虚拟化。容器是完全使用沙箱机制&#xff0c;相互…

Elasticsearch:在满意度调查中实现并使用情绪分析器

如果你通过博客或新闻关注 Elastic&#xff0c;你已经知道在最新版本的 Elasticsearch 中已经提供了用于自然语言处理 (NLP) 的资源。事实上&#xff0c;在我之前的博客文章中&#xff0c;我已经推出了很多关于 NLP 的博文。请详细阅读 “Elastic&#xff1a;开发者上手指南” …

【Linux 多线程同步】使用同步和互斥实现生产消费模型

目录 1.同步的接口 2.多线程但是按顺序来执行 3.生产消费模型 4.使用互斥加同步实现生产消费模型 &#xff08;采用环形队列&#xff09; 同步&#xff1a;在保证数据安全的前提下&#xff0c;让线程能够按照某种特定的顺序访问临界资源&#xff0c;从而有效避免饥饿问题 …

GraphTrip论文笔记【Information Fusion 92 (2023)】

Dual-grained human mobility learning for location-aware trip recommendation with spatial–temporal graph knowledge fusion 期刊&#xff1a;EI检索 Information Fusion 92 (2023) Challenges &#xff08;1&#xff09;异质交互信息的挖掘 POI位置信息、POI类别信息…