jdk21 虚拟线程原理及使用分享

虚拟线程概述

jdk21已于北京时间9月19日21点正式发布, 其中引人注目的就是虚拟线程(Virtual Thread)随之正式发布, 不再是此前jdk19、jdk20中的预览版本。
平台线程:java传统的线程是对系统线程的包装,为了区别于虚拟线程,因此将通过传统方式实现的线程叫做平台线程(Platform Thread)
虚拟线程:虚拟线程是由JDK内部实现的轻量级线程,不依赖于操作系统,可以显著减少编写、维护和观察高吞吐量并发应用程序的工作量。

jdk为什么增加虚拟线程

添加虚拟线程工作量巨大,花费了数年时间,不断孵化,虚拟线程主要是为了解决异步编程相关的问题, 让应用程序能够以简单的一个请求一个处理线程的方式运行,并且能够达到硬件的最佳利用率,先回顾下java传统方式实现编发的两个方案:
在这里插入图片描述

一个请求一个处理线程

这种方式可以让开发专注于业务逻辑,使用命令式编程,代码在一条线程上从头到尾执行, Tomcat的Servlet线程就是该模式。
为了提高应用程序的并发请求数,通常会启用多个线程来接受请求,jdk中的线程是对操作系统线程的包装。这导致java的线程创建,销毁成本比较高,为了避免这种情况通常会使用线程池来提高程序性能。
假如一个请求需要耗时50ms,要想实现每秒200的吞吐量, 则理论上至少需要10条线程。如果要想达到2000的吞吐量,怎需要将线程池线程数量设置到100条。

缺点

但一个操作系统能创建的线程数量是有限的,线程池化虽然避免了线程创建、销毁的开销,但并不能提高线程数。在CPU和连接数被耗尽之前很可能无法再创建线程,CPU也就无法得到充分利用。

通过异步方式提高可扩展性

为了充分提高硬件利用率,则出现了类似netty这种异步事件驱动的网络I/O框架,以及Reactive Stream这种反应式编程模式(Spring-WebFlux就是反应式编程的一种实现)。代码不是在一个线程上从头到尾处理请求,而是在等待另一 I/O 操作完成时将其线程返回到池中,以便该线程可以为其它请求提供服务,可以实现通过少量线程数达到大量并发操作。
但是这种编程方式比较难以维护,通过大量的回调方法编排业务逻辑(通常是使用java8的lambda语法实现),方法的返回值变成了Mono、或者CompletableFuture类型。大量回调不便于理解业务需求,需要在onErrorResume中做异常处理,也无法对整个方法加try/catch块达到预期的异常处理。
在这里插入图片描述
如上是一个Spring-webflux的示例代码,一个方法存在4层return语句,并且这4层代码块很可能是运行在不同线程。

存在的缺点:

  1. 代码运行在不同的线程,堆栈跟踪无法提供可用的上下文,调试器无法单步执行请求处理逻辑
  2. 无法通过ThreadLocal传值.
  3. 难以专注业务逻辑,当想扩展业务逻辑时, 不知道在何处编写自己的代码, 在对反应式编程不熟悉的情况下可能在线程中执行耗时操作, 导致线程阻塞

使用虚拟线程来达到一个请求一个处理线程

JDK传统方式实现的平台线程是对操作系统线程包装,线程的创建受到操作系统的限制,一条平台线程的创建要占用到1M左右的内存。
虚拟线程是JDK基于平台线程实现的轻量级线程虚拟线程依附于平台线程(此时称为载体线程)运行,它的创建成本很低,不会像平台线程独占操作系统线程,Java 通过将大量虚拟线程映射到少量平台线程来提供充足线程的假象。
因此可以通过虚拟线程来实现一次请求代码只会执行在同一个虚拟线程中,让开发者更专注于业务逻辑。
但虚拟线程仅在 CPU 上执行计算时才消耗操作系统线程,有着与异步编程方式相同的吞吐量,只不过它是透明实现的:当在虚拟线程中运行的代码调用阻塞 I/O 操作时,java会自动挂起虚拟线程,IO操作完成后再自动恢复执行虚拟线程。


为什么增加虚拟线:简单直白的描述就是希望java开发人员能够以简单易懂的编码方式来实现高吞吐量量的应用程序,提高CPU利用率,避免资源浪费。


非虚拟线程目标

  1. 并不是想替换传统的线程实现,也并不是为了让所有应用程序全部切换到虚拟线程
  2. 并不是为了改变基本的并发模型, 也就是说原来的线程、锁、条件变量、信号量、阻塞队列该用还是得用
  3. 并不是为了提供新的并行结构, Stream API仍然是并行处理数据集的首选方式
    使用虚拟线程注意事项
  4. 虚拟线程便宜且充足,因此永远不应该被池化,每次使用时应该创建一个新的虚拟线程。
  5. 池化虚拟线程甚至可能带来性能上的影响,例如之前可能通过ThreadLocal来保存创建的大对象,来避免每次创建,但是大量的虚拟线程可能导致创建大量大对象而影响性能。
  6. 如果要想要控制并发量, 可以通过java.util.concurrent.Semaphore信号量这样的方式控制
  7. 虚拟线程始终是守护线程。该Thread.setDaemon(boolean)方法无法将虚拟线程更改为非守护线程。
  8. 虚拟线程具有固定的优先级Thread.NORM_PRIORITY。该Thread.setPriority(int)方法对虚拟线程没有影响。未来版本中可能会重新考虑此限制。
  9. 虚拟线程在集合运行时没有权限SecurityManager。
  10. 该java.lang.management.ThreadMXBeanAPI支持平台线程的监控和管理,但不支持虚拟线程。
  11. 该-XX:+PreserveFramePointer标志对虚拟线程性能有巨大的负面影响

虚拟线程的使用

示例1

创建虚拟线程并直接启动
Thread.startVirtualThread(new Runnable() {
@Override
public void run() {
log.info(“虚拟线程执行:threadId:{}”, Thread.currentThread().threadId());
}
});
创建2条虚拟线程调用代码启动,并等待执行完成
// 这种写法会让日志中无法打印线程名
public static void main(String[] args) throws InterruptedException {
var vthread = Thread.ofVirtual().unstarted(() -> {
log.info(“虚拟线程休眠开始:” + Thread.currentThread());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info(“虚拟线程休眠结束:” + Thread.currentThread());
});

var vthread2 = Thread.ofVirtual().unstarted(() -> {
    log.info("虚拟线程休眠开始:" + Thread.currentThread());
    try {
        Thread.sleep(110);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    log.info("虚拟线程休眠结束:" + Thread.currentThread());
});

vthread.start();
vthread2.start();
vthread2.join();
vthread.join();

}
如下是执行结果:
[图片]
通过这个日志可以发现两个问题:

  1. 日志中没能打印出线程名
    给虚拟线程添加线程名,便于业务分析,排障
    // 给虚拟线程添加线程名,便于业务分析,排障
    public static void main(String[] args) throws InterruptedException {
    var vthread = Thread.ofVirtual().name(“vThread-test-”, 1).unstarted(() -> {
    log.info(“虚拟线程休眠结束:{}”, Thread.currentThread());
    });

    var vthread2 = Thread.ofVirtual().name(“vThread-test2-”, 2).unstarted(() -> {
    log.info(“虚拟线程休眠结束:{}”, Thread.currentThread());
    });

    vthread.start();
    vthread2.start();
    vthread2.join();
    vthread.join();
    }
    实际上Thread.ofVirtual().name(“vThread-test-”, 1)返回的Thread.Builder.OfVirtual是
    VirtualThreadBuilder实现类,并不需要每次创建,可以改成如下方式:
    public static void main(String[] args) throws InterruptedException {
    Thread.Builder.OfVirtual virtualThreadBuilder
    = Thread.ofVirtual()
    .name(“vThread-test-”, 1)
    .uncaughtExceptionHandler((t, e) -> System.out.println(“异常处理”));

    var vthread = virtualThreadBuilder.unstarted(() -> {
    log.info(“虚拟线程休眠结束:{}”, Thread.currentThread());
    });

    var vthread2 = virtualThreadBuilder.unstarted(() -> {
    log.info(“虚拟线程休眠结束:{}”, Thread.currentThread());
    });

    vthread.start();
    vthread2.start();
    vthread2.join();
    vthread.join();
    }
    也可以使用创建线程的工厂模式,
    这种方式估计是为了适配原先的java.util.concurrent.ThreadFactory
    ThreadFactory factory = Thread.ofVirtual()
    .name(“virtual-thread-test-2-”, 1)
    .uncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
    @Override
    public void uncaughtException(Thread t, Throwable e) {
    System.out.println(“虚拟线程触发异常” + t + “,throwable:” + e.getMessage());
    log.info(“虚拟线程Id:{}, 虚拟线程名:{}, 依附的平台线程:{}”, t.threadId(), t.getName(), t);
    }
    })
    .factory();
    // 通过工厂创建虚拟线程
    factory.newThread(runnable);
    [图片]

  2. 再看日志的第二个问题,虚拟线程号#23开始和结束日志后面的ForkJoinPool-1-worker-后面的序号不一样,这是为什么???
    #23是虚拟线程的线程号,而后面的ForkJoinPool-1-worker-实际上是虚拟线程所被挂载到的平台线程(载体线程),从名字可以看出,虚拟线程依赖的载体线程实际上由ForkJoinPool来实现,jdk在调度虚拟线程时保证代码在挂起前后是在同一个虚拟线程执行,但是不保证所依赖的载体线程也是同一个,也不需要有这样的保证。
    java.lang.VirtualThread#toString方法
    [图片]
    示例2
    下面是JDK官网一个创建大量虚拟线程的示例程序。程序首先获得一个ExecutorService将为每个提交的任务创建一个新的虚拟线程。然后它提交 10,000 个任务并等待所有任务完成:
    try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 10_000).forEach(i -> {
    executor.submit(() -> {
    Thread.sleep(Duration.ofSeconds(1));
    return i;
    });
    });
    } // executor.close() is called implicitly, and waits
    此示例中的任务是简单的代码(休眠一秒钟),现代硬件可以轻松支持 10,000 个虚拟线程同时运行此类代码。但实际上却仅依赖少量的平台线程

  3. 如果这个程序使用Executors.newCachedThreadPool()创建ExecutorService,ExecutorService将尝试创建 10,000 个平台线程,从而创建 10,000 个操作系统线程,可能会出现程序崩溃,具体取决于机器性能和操作系统,我实验的时候电脑直接崩了重启,估计是内存不足导致。

  4. 如果程序使用Executors.newFixedThreadPool(200)创建ExecutorService,ExecutorService将创建 200 个平台线程,10,000 个任务共用该线程池,许多任务将顺序运行而不是并发运行,并且程序将需要很长时间才能完成。对于该程序,具有 200 个平台线程的池只能实现每秒 200 个任务的吞吐量,而虚拟线程可实现每秒约 10,000 个任务的吞吐量。此外,如果将10_000示例程序中的 更改为1_000_000,则该程序将提交 1,000,000 个任务,创建 1,000,000 个并发运行的虚拟线程,并且(在充分预热后)实现每秒约 1,000,000 个任务的吞吐量。

  5. 如果该程序中的任务执行一秒钟的计算(例如,对一个巨大的数组进行排序),而不是仅仅休眠,无论它们是虚拟线程还是平台线程,只要线程数量超出处理器核心数量都没有效果。虚拟线程并不能让运行代码的速度比平台线程快,但可以显著提高应用程序吞吐量。

  6. 虚拟线程可以运行平台线程可以运行的任何代码。特别是,虚拟线程支持ThreadLocal和线程中断,就像平台线程一样。这意味着处理请求的现有 Java 代码可以轻松地在虚拟线程中运行。许多服务器框架会选择自动执行此操作,为每个传入请求启动一个新的虚拟线程并在其中运行应用程序的业务逻辑。
    可以将示例中的测试数据改小点看下效果:
    使用平台线程, 程序吞吐量受到线程数影响,只有2条并发,其他任务在等待平台线程释放。
    [图片]
    而使用虚拟线程几乎同时执行完成
    [图片]
    以上示例使用Executor.newVirtualThreadPerTaskExecutor()直接创建虚拟线程,同样丢失了虚拟线程名,可以通过虚拟线程工来添加虚拟线程名
    ThreadFactory factory = Thread.ofVirtual().name(“virtual-thread-test-”, 1).factory();
    ExecutorService virtualThreadExecutor = Executors.newThreadPerTaskExecutor(factory);
    [图片]
    示例3
    下面是聚合两个远程请求结果并作为返回值的示例:
    public Response handle(Request request) {
    var url1 = …
    var url2 = …
    Response response = new Response();
    try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    var future1 = executor.submit(() -> fetchURL(url1));
    var future2 = executor.submit(() -> fetchURL(url2));
    response.send(future1.get() + future2.get());
    } catch (ExecutionException | InterruptedException e) {
    response.fail(e);
    }
    return response;
    }

String fetchURL(URL url) throws IOException {
try (var in = url.openStream()) {
return new String(in.readAllBytes(), StandardCharsets.UTF_8);
}
}
handle方法中通过虚拟线程访问2个远程服务,并通过future1.get()等待结果,将结果聚合返回。像这样的服务器应用程序具有简单的阻塞代码,可以很好地扩展,因为它可以使用大量虚拟线程。
虚拟线程调度原理
JDK传统的平台线程依赖于操作系统调度。
[图片]
而对于虚拟线程,由JDK 调度执行。JDK的调度程序将虚拟线程分配给平台线程(此时平台线程称为虚拟线程的载体)。然后,操作系统像往常一样调度平台线程,(可以实现虚拟线程和平台线程M:N调度关系)。
[图片]
JDK的虚拟线程调度程序通过ForkJoinPool以先进先出(FIFO)模式调度。调度程序默认的平台线程数它等于可用处理器的数量,可以通过系统属性进行调整jdk.virtualThreadScheduler.parallelism。
源码见: java.lang.VirtualThread#createDefaultScheduler
[图片]
虚拟线程在其生命周期内可以被调度到不同的载体上;换句话说,调度程序不维护虚拟线程和任何特定平台线程之间的关联性。从Java代码的角度来看,一个正在运行的虚拟线程在逻辑上独立于它当前的载体:

  • 虚拟线程无法获取载体线程,Thread.currentThread()返回的始终是虚拟线程本身。
  • 载体和虚拟线程的堆栈跟踪是分开的。虚拟线程中抛出的异常将不包括载体线程的堆栈帧。线程转储不会显示虚载体线程的堆栈帧,反之亦然。
  • 载体的线程局部变量对于虚拟线程不可用,反之亦然。

源码级分析参考: 虚拟线程 - VirtualThread源码透视
虚拟线程的挂载和卸载
当JDK调度程序调度虚拟线程执行时则为挂载,此时平台线程成为载体线程
当虚拟线程执行完成或被阻塞时则由调度程序从载体线程卸载,平台线程可以再次用于挂载其它虚拟线程执行
如下会触发虚拟线程卸载:

  1. 虚拟线程在 I/O 阻塞, 例如一次网络请求

  2. 队列的阻塞等待BlockingQueue.take()

  3. JDK中的绝大多数阻塞操作都会卸载虚拟线程, 这些操作对用户透明,无需额外代码。代码块中存在多个阻塞操作, 会导致虚拟线程多次执行挂载和卸载
    然而,JDK中的一些阻塞操作不会卸载虚拟线程,从而阻塞其载体和底层操作系统线程。这是因为操作系统级别(例如,许多文件系统操作)或 JDK 级别(例如,Object.wait())的限制。因此,调度程序中的平台线程数量可能会暂时超过可用处理器的数量。可以通过系统参数jdk.virtualThreadScheduler.maxPoolSize设置最大平台线程数,默认是256。
    另外如下两种情况,会导致虚拟线程被固定在载体线程上, 从而导致无法被卸载:

  4. 执行的代码是一个synchronized方法或者存在synchronized代码块,(未来的jdk版本中可能考虑消除该限制)

  5. 执行的代码中调用native方法或通过JNI实现的一个外部函数时。(但是这个目前看来是无法消除的限制)
    虚拟线程被固定到载体线程可能会导致其它虚拟线程无法得到载体线程执行,JDK提供了如下的监控方式,便于查找出被固定到载体线程的虚拟线程,并根据情况使用ReentrantLock替换synchronized

  6. 当线程在固定状态下阻塞时,会发出 JDK Flight Recorder (JFR) 事件(请参阅JDK Flight Recorder)。

  7. 当线程在固定状态下阻塞时,开启系统属性-Djdk.tracePinnedThreads=full会打印完整的堆栈跟踪,突出显示native帧和持有监视器的帧。-Djdk.tracePinnedThreads=short仅输出有问题的帧。
    隔离载体线程(重要)
    jdk默认所有的虚拟线程都共用一个载体线程,如果虚拟线程出现上述出现的固定在载体线程执行等待的情况,可能导致其它线程无法得到载体线程执行,因此有必要隔离载体线程。
    [图片]
    [图片]
    [图片]
    jdk没有开放自定义载体线程的方式,但是可以通过反射来设置自定义的载体线程
    public static void main(String[] args) throws Exception {

    ForkJoinPool forkJoinPool = ForkJoinPoolFactory.createDefaultScheduler(“custom-platform-thread-”);

    Thread.Builder.OfVirtual virtualBuilder = Thread.ofVirtual();
    Field schedulerField = virtualBuilder.getClass().getDeclaredField(“scheduler”);
    schedulerField.setAccessible(true);
    schedulerField.set(virtualBuilder, forkJoinPool);

    virtualBuilder.name(“virtual-thread-test-”, 0)
    .uncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
    @Override
    public void uncaughtException(Thread t, Throwable e) {
    System.out.println(“异常处理”);
    }
    })
    .factory();

    for (int i = 0; i < 10; i++) {
    virtualBuilder.start(new Runnable() {
    @Override
    public void run() {
    log.info(“虚拟线程执行:thread:{}”, Thread.currentThread());
    }
    });
    }

    Thread.sleep(Duration.ofSeconds(3));
    }
    运行结果: 可以看到, 载体线程被替换成自定义的平台线程
    [图片]
    虚拟线程的观测
    观察正在运行的程序的状态对于故障排除、维护和优化也至关重要,虚拟线程也是线程的一种实现,因此常见的监控工具也能监控虚拟线程。

  8. Java 调试器可以单步执行虚拟线程、显示调用堆栈并检查堆栈帧中的变量。

  9. JDK Flight Recorder (JFR) 是 JDK 的低开销分析和监视机制,可以将应用程序代码中的事件(例如对象分配和 I/O 操作)与正确的虚拟线程关联起来。

  10. JDK传统jstack或jcmd命令仅适用于数十或数百个平台线程,但不适合数千或数百万虚拟线程。因此JDK引入一种新的线程转储,jcmd以将虚拟线程与平台线程一起呈现,jcmd除了纯文本之外,还可以以 JSON 格式保存线程转储信息:
    $ jcmd Thread.dump_to_file -format=json
    但新的线程转存储格式不包括对象地址、锁、JNI 统计信息、堆统计信息以及传统线程转储中出现的其他信息。
    由于可能存在大量线程,因此JDK将这种线程dump方式设计为不暂停应用程序。
    如果设置系统属性-Djdk.trackAllThreads=false,则直接使用java.lang.Thread.BuilderAPI 创建的虚拟线程将不会被运行时跟踪,并且可能不会出现在线程dump信息中。只会列出阻塞在网络io操作的虚拟线程以及由Executors.newVirtualThreadPerTaskExecutor()创建的虚拟线程。
    以下是此类线程转储的示例,取自与上面第二个示例类似的应用程序,在 JSON 查看器中呈现(单击可放大):

  11. 虚拟线程是在 JDK 中实现的,并且不依赖于任何特定的操作系统线程,因此操作系统级监控无法观察到虚拟线程。
    内存使用以及与垃圾收集的交互

  12. 虚拟线程的堆栈作为堆栈块对象存储在 Java 的垃圾收集堆中。堆栈随着应用程序的运行而增长和缩小,既是为了提高内存效率,也是为了容纳深度达到 JVM 配置的平台线程堆栈大小的堆栈。这种效率使得大量虚拟线程成为可能,从而使服务器应用程序中按请求线程的方式保持持续的可行性。

  13. 与平台线程堆栈不同,虚拟线程堆栈不是 GC 根。因此,它们包含的引用不会被执行并发堆扫描的垃圾收集器(例如 G1)在停止世界暂停的情况下遍历。

  14. 与平台线程相比,在虚拟线程上运行此类工作负载有助于减少内存占用

  15. 当前虚拟线程存在的限制是由于G1 GC 不支持巨大的堆栈块对象。如果虚拟线程的堆栈达到区域大小的一半(可能小至 512KB),则StackOverflowError可能会抛出异常。
    线程局部变量
    虚拟线程支持线程局部变量 ( ThreadLocal) 和可继承的线程局部变量 ( InheritableThreadLocal),就像平台线程一样,因此它们可以运行使用线程局部变量的现有代码。但是,由于虚拟线程可能非常多,因此只有在仔细考虑后才能使用线程局部变量。特别是,不要使用线程局部变量在线程池中共享同一线程的多个任务之间池化昂贵的资源。虚拟线程永远不应该被池化,因为每个虚拟线程在其生命周期内只运行一个任务。我们从 JDKjava.base模块中删除了许多线程局部变量的使用,为虚拟线程做好准备,以便在运行数百万个线程时减少内存占用。
    当虚拟线程设置任何线程局部变量的值时,系统属性jdk.traceVirtualThreadLocals可用于触发堆栈跟踪。当迁移代码以使用虚拟线程时,此诊断输出可能有助于删除线程局部变量。将系统属性设置为true来触发堆栈跟踪;默认值为false。
    对于某些用例,ScopedValue(JEP 429)是线程局部变量的更好替代方案, 但JDK21中仍然是预览版本。
    有栈协程和无栈协程

  16. 有栈协程是指协程在运行时需要使用栈来保存函数调用的上下文信息,例如局部变量、函数返回地址等。当协程挂起时,栈中的上下文信息会保存下来,以便下次恢复执行。栈协程的优点是可以方便地保存和恢复函数调用的上下文信息,但缺点是需要为每个协程分配一定的栈空间。

  17. 无栈协程是指协程在运行时不需要使用栈来保存函数调用的上下文信息,而是使用状态机来保存协程的状态。当协程挂起时,当前的状态会被保存下来,以便下次恢复执行。无栈协程的优点是不需要为每个协程分配栈空间,节省了内存,但缺点是需要手动实现状态机来保存和恢复协程的状态,代码复杂度较高。
    JDK21为什么使用有栈协程:JDK21虚拟线程的文档中指出: 将无堆栈协程(即async/await)添加到 Java 语言中,比用户模式线程更容易实现,但是这会导致使用发生较大的变化,一些监控工具需要发生大的变更,会导致需要更长的时间才能被java生态采用,栈协程迁移简单。
    拥抱虚拟线程
    Spring
    Spring Framework、Spring Boot已经适配了虚拟线程
    在SpringBoot中只需要通过如下配置即可让Spring异步任务,Servlet使用虚拟线程
    @Bean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)
    public AsyncTaskExecutor asyncTaskExecutor() {
    return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
    }

@Bean
public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
return protocolHandler -> {
protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
};
}
Spring在积极的改进相关代码,已适配虚拟线程, 例如数据库驱动程序、消息传递系统、HTTP 客户端等等。
但也表示虚拟线程不能完全替换ReactiveX 的编程模式,但是可以补充ReactiveX 中的一些不足.
Spring相关blog: https://spring.io/blog/2022/10/11/embracing-virtual-threads
Tomcat
[图片]
Netty
[图片]

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Git】工作中的留痕:分支及标签的超神搭配

&#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 接下来看看由辉辉所写的关于Git的相关操作吧 目录 &#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 一.Git分支是什么 二.Git分支的使用 1.分…

HTTPS协议

目录 HTTPS概念加密是什么常见加密方式对称加密非对称加密数据摘要&&数据指纹数据签名 HTTP工作过程探究方案一&#xff1a;只使用对称加密方案二&#xff1a;只使用非对称加密方案三&#xff1a;双方都使用非对称加密方案四&#xff1a;非对称加密对称加密中间人攻击 …

常见面试题-JDK和CGLIB动态代理

JDK 动态代理和 CGLIB 动态代理对比 JDK 动态代理只能代理实现了接口的类&#xff0c;而 CGLIB 可以代理未实现任何接口的类。另外CGLIB 动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用&#xff0c;因此不能代理声明为final 类型的类和方法就二者的效率来说&a…

Unbuntu安装、测试和卸载gcc11

GCC 可用于编译 C、C&#xff0c;本文介绍如何 Ubuntu 上安装 gcc11、测试和卸载它。 1. 在Ubuntu 上安装 gcc11 添加工具链存储库 sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test在 Ubuntu 上安装 gcc11 sudo apt install -y gcc-11验证 gcc11 版本 gcc-11 --v…

AI:80-基于深度学习的医学图像分割与病变识别

🚀 本文选自专栏:人工智能领域200例教程专栏 从基础到实践,深入学习。无论你是初学者还是经验丰富的老手,对于本专栏案例和项目实践都有参考学习意义。 ✨✨✨ 每一个案例都附带有在本地跑过的代码,详细讲解供大家学习,希望可以帮到大家。欢迎订阅支持,正在不断更新中,…

数据架构与数据模型

数据架构&#xff1a; 待定 数据模型&#xff1a; 数据模型是对现实世界数据特征的抽象&#xff0c;用于描述一组数据的概念和定义。数据模型从抽象层次上描述了数据的静态特征、动态行为和约束条件。数据模型所描述的内容有三部分&#xff0c;分别是数据结构、数据操作和数…

数据结构与算法 | 第四章:字符串

本文参考网课为 数据结构与算法 1 第四章字符串&#xff0c;主讲人 张铭 、王腾蛟 、赵海燕 、宋国杰 、邹磊 、黄群。 本文使用IDE为 Clion&#xff0c;开发环境 C14。 更新&#xff1a;2023 / 11 / 12 数据结构与算法 | 第四章&#xff1a;字符串 字符串概念字符串字符字符…

rocksdb中测试工具Benchmark.sh用法(基准、性能测试)

1.首先要安装db_bench工具&#xff0c;这个工具在成功安装rocksdb之后就自动存在了&#xff0c;主要是在使用make命令之后就成功安装了&#xff0c;详情请见我之前的文章 2.确保成功安装db_bench之后&#xff0c;找到安装的rocksdb目录下面的tools文件夹&#xff0c;查看里面是…

如何让VirtualBox系统使用Ubuntu主机的USB

如何让VirtualBox系统使用Ubuntu主机的USB 当通过 VirtualBox 尝试不同的操作系统时&#xff0c;访问虚拟机中的 USB 驱动器来传输数据非常有用。 安装Guest Additions 自行百度安装Guest Additions的方法&#xff0c;最终的效果如下&#xff1a; 将用户添加到 vboxusers 组…

前端面试题 计算机网络

文章目录 ios 7层协议tcp协议和udp协议的区别tcp协议如何确保数据的可靠http和tcp的关系url输入地址到呈现网页有哪些步骤post和get本质区别&#xff0c;什么时候会触发二次预检GET请求&#xff1a;POST请求&#xff1a;触发二次预检&#xff08;CORS中的预检请求&#xff09;&…

通过结构间比值比较迭代次数

( A, B )---3-30-2---( 1, 0 )( 0, 1 ) 让网络的输入只有3个节点&#xff0c;A有5个点&#xff0c;B全是0&#xff0c;排列组合。让A,B训练集分别有3&#xff0c;4&#xff0c;5&#xff0c;6张图片&#xff0c;统计迭代次数并排序。 先比较图片数量是3和4的情况 n4 迭代次数…

移植LVGL到单片机的一个demo简单介绍

简介 背景&#xff1a; 本文使用的是主控IC为stm32f103zet6, 显示IC为ST7735s&#xff0c;它是128*160的像素&#xff0c;色深为RGB565颜色。 官方虽然说LVGL移植平台只需 64kB 闪存和 8kB RAM 就足以满足简单的用户界面。但我移植到stm32f103c8t6&#xff0c;不管怎么修改配…

【数据结构】入队序列出队序列问题(以21年408真题举例)

题型说明 一般是一个队列&#xff0c;其中一边可以入队&#xff0c;另一边可以入队和出队只可入队的含义是从这个方向是以队列形式存在可以入队和出队表示此边以堆形式存在 怎么分析&#xff1f; 以21年408真题举例 考点分析 出队序列存在两种情况&#xff1a;入之后就出&…

是谁为所欲为,将我的电脑控作己用?

在刚刚发完短篇小杂文《要找事做&#xff0c;我真怕被闲死》的投稿之后&#xff0c;笔者继续浏览社交网站的网页搜索...... 正看到《温州殡仪馆 》《温州动车723事故死亡高 》《 动车脱轨温州事件真正原因》《 浙江平阳县灭门惨案处理结果公布》《 温州厉秀珍死亡 》这一串又一…

从HDFS到对象存储,抛弃Hadoop,数据湖才能重获新生?

Hadoop与数据湖的关系 1、Hadoop时代的落幕2、Databricks和Snowflake做对了什么3、Hadoop与对象存储&#xff08;OSD&#xff09;4、Databricks与Snowflake为什么选择对象存储5、对象存储面临的挑战 1、Hadoop时代的落幕 十几年前&#xff0c;Hadoop是解决大规模数据分析的“白…

Qt开发流程

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、pandas是什么&#xff1f;二、使用步骤 1.引入库2.读入数据总结 前言 提示&#xff1a;这里可以添加本文要记录的大概内容&#xff1a; 例如&#xff1a;…

SpringBoot整合第三方技术

SpringBoot整合JUnit 名称&#xff1a;SpringBootTest 类型&#xff1a;测试类注解 位置&#xff1a;测试类定义上方 作用&#xff1a;设置JUnit加载的SpringBoot启动类SpringBootTest(classes Springboot05JUnitApplication.class) class Springboot07JUnitApplicationTests…

腾讯云3年轻量2核4G5M服务器756元,抓紧数量不多

腾讯云轻量应用服务器特价是有新用户限制的&#xff0c;所以阿腾云建议大家选择3年期轻量应用服务器&#xff0c;一劳永逸&#xff0c;免去续费困扰。腾讯云轻量应用服务器3年可以选择2核2G4M和2核4G5M带宽&#xff0c;3年轻量2核2G4M服务器540元&#xff0c;2核4G5M轻量应用服…

redis学习指南--概览篇

redis怎么学 官方学习网站&#xff1a; redis.cn 1、整体了解redis redis是一个内存数据库、kv数据库&#xff0c;数据结构数据库&#xff0c;redis中数据都是存储在redis中&#xff0c;可以通过key查找value&#xff0c;value可以有多种数据结构&#xff0c;有&#xff1a;…

归并外排序实现

文章目录 1. 海量数据排序 1. 海量数据排序 如果我们想在文件中海量数据排序&#xff0c;我们比较适合选用归并排序。 首先&#xff0c;我们要看要排序的文件的大小&#xff0c;比如说这个文件是10G&#xff0c;而我们的内存是1G&#xff0c;那么我们可以把文件切成10份。这样…
最新文章