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

news2024/11/16 15:48:57

虚拟线程概述

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 个虚拟线程同时运行此类代码。但实际上却仅依赖少量的平台线程

  1. 如果这个程序使用Executors.newCachedThreadPool()创建ExecutorServiceExecutorService将尝试创建 10,000 个平台线程,从而创建 10,000 个操作系统线程,可能会出现程序崩溃,具体取决于机器性能和操作系统,我实验的时候电脑直接崩了重启,估计是内存不足导致。
  2. 如果程序使用Executors.newFixedThreadPool(200)创建ExecutorServiceExecutorService将创建 200 个平台线程,10,000 个任务共用该线程池,许多任务将顺序运行而不是并发运行,并且程序将需要很长时间才能完成。对于该程序,具有 200 个平台线程的池只能实现每秒 200 个任务的吞吐量,而虚拟线程可实现每秒约 10,000 个任务的吞吐量。此外,如果将10_000示例程序中的 更改为1_000_000,则该程序将提交 1,000,000 个任务,创建 1,000,000 个并发运行的虚拟线程,并且(在充分预热后)实现每秒约 1,000,000 个任务的吞吐量。
  3. 如果该程序中的任务执行一秒钟的计算(例如,对一个巨大的数组进行排序),而不是仅仅休眠,无论它们是虚拟线程还是平台线程,只要线程数量超出处理器核心数量都没有效果。虚拟线程并不能让运行代码的速度比平台线程快,但可以显著提高应用程序吞吐量
  4. 虚拟线程可以运行平台线程可以运行的任何代码。特别是,虚拟线程支持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));
}

运行结果: 可以看到, 载体线程被替换成自定义的平台线程
在这里插入图片描述

虚拟线程的观测

观察正在运行的程序的状态对于故障排除、维护和优化也至关重要,虚拟线程也是线程的一种实现,因此常见的监控工具也能监控虚拟线程。

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

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

  3. JDK传统jstack或jcmd命令仅适用于数十或数百个平台线程,但不适合数千或数百万虚拟线程。因此JDK引入一种新的线程转储,jcmd以将虚拟线程与平台线程一起呈现,jcmd除了纯文本之外,还可以以 JSON 格式保存线程转储信息:

    $ jcmd <pid> Thread.dump_to_file -format=json <file>
    

    但新的线程转存储格式不包括对象地址、锁、JNI 统计信息、堆统计信息以及传统线程转储中出现的其他信息。
    由于可能存在大量线程,因此JDK将这种线程dump方式设计为不暂停应用程序。
    如果设置系统属性-Djdk.trackAllThreads=false,则直接使用java.lang.Thread.BuilderAPI 创建的虚拟线程将不会被运行时跟踪,并且可能不会出现在线程dump信息中。只会列出阻塞在网络io操作的虚拟线程以及由Executors.newVirtualThreadPerTaskExecutor()创建的虚拟线程。

  4. 虚拟线程是在 JDK 中实现的,并且不依赖于任何特定的操作系统线程,因此操作系统级监控无法观察到虚拟线程。

内存使用以及与垃圾收集的交互

  1. 虚拟线程的堆栈作为堆栈块对象存储在 Java 的垃圾收集堆中。堆栈随着应用程序的运行而增长和缩小,既是为了提高内存效率,也是为了容纳深度达到 JVM 配置的平台线程堆栈大小的堆栈。这种效率使得大量虚拟线程成为可能,从而使服务器应用程序中按请求线程的方式保持持续的可行性。
  2. 与平台线程堆栈不同,虚拟线程堆栈不是 GC 根。因此,它们包含的引用不会被执行并发堆扫描的垃圾收集器(例如 G1)在停止世界暂停的情况下遍历。
  3. 与平台线程相比,在虚拟线程上运行此类工作负载有助于减少内存占用
  4. 当前虚拟线程存在的限制是由于G1 GC 不支持巨大的堆栈块对象。如果虚拟线程的堆栈达到区域大小的一半(可能小至 512KB),则StackOverflowError可能会抛出异常。

线程局部变量

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

有栈协程和无栈协程

  1. 有栈协程是指协程在运行时需要使用栈来保存函数调用的上下文信息,例如局部变量、函数返回地址等。当协程挂起时,栈中的上下文信息会保存下来,以便下次恢复执行。栈协程的优点是可以方便地保存和恢复函数调用的上下文信息,但缺点是需要为每个协程分配一定的栈空间。
  2. 无栈协程是指协程在运行时不需要使用栈来保存函数调用的上下文信息,而是使用状态机来保存协程的状态。当协程挂起时,当前的状态会被保存下来,以便下次恢复执行。无栈协程的优点是不需要为每个协程分配栈空间,节省了内存,但缺点是需要手动实现状态机来保存和恢复协程的状态,代码复杂度较高。
    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.coloradmin.cn/o/1201006.html

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

相关文章

C#源代码生成器深入讲解一

C#源代码生成器 01 源代码生成器初体验 新建一个类库&#xff0c;一定是standard2.0版本&#xff0c;否则会出问题。引用Nuget包Microsoft.CodeAnalysis.Common新建一个类&#xff0c;继承自ISourceGenerator接口 //一定要写&#xff0c;制定语言 [Generator(LanguageNames.…

Django 基于ORM的CURD、外键关联,请求的生命周期

文章目录 基于ORM进行的CURDORM外键关联Django请求的生命周期流程图 基于ORM进行的CURD 本质上就是通过面向对象的方式&#xff0c;对数据库的数据进行增、删、改、查。 这里将会将我们之前所有内容结合到一起&#xff0c;首先确保基于上序操作已经建立好了UserInfo表&#xff…

Three.js——基于原生WebGL封装运行的三维引擎

文章目录 前言一、什么是WebGL&#xff1f;二、Three.js 特性 前言 Three.js中文官网 Three.js是基于原生WebGL封装运行的三维引擎&#xff0c;在所有WebGL引擎中&#xff0c;Three.js是国内文资料最多、使用最广泛的三维引擎。既然Threejs是一款WebGL三维引擎&#xff0c;那么…

Python 使用tkinter的Scrollbar方法创建Text水平和垂直滚动条

在Python的Tkinter中&#xff0c;可以使用Scrollbar来实现Text组件的上下或左右滑动。首先&#xff0c;需要创建一个Scrollbar对象并将其与Text组件绑定&#xff0c;然后将Scrollbar放置在Text组件的右侧或底侧&#xff0c;使其能够控制Text组件的上下或左右滑动。 运行结果&am…

隔离在高可用架构中的使用

写作目的 最近看到了河北王校长隔离的视频&#xff0c;结合自己在工作中的应用&#xff0c;分享常见的隔离落地方案。 隔离落地方案 服务环境隔离 因为我们的项目服务于整个国内的多条产品线&#xff0c;也服务于国外。为了低成本所以使用一套代码。在产品线之间隔离&#…

mysql讲解2 之事务 索引 以及权限等

系列文章目录 mysql 讲解一 博客链接 点击此处即可 文章目录 系列文章目录一、事务1.1 事务的四个原则1.2 脏读 不可重复读 幻读 二、索引三,数据库用户管理四、mysql备份 一、事务 1.1 事务的四个原则 什么是事务 事务就是将一组SQL语句放在同一批次内去执行 如果一个SQ…

深入研究SVN代码检查的关键工具:svnchecker vs. SonarQube

目录 一、SVN代码检查(整合svnchecker)1、创建SVN代码库2、下载安装包3、修改SVN配置4、新建代码检查配置文件(名称自定义)5、hooks目录添加配置文件6、设置只对Java文件进行检查7、测试 二、SonarQube代码检测1、什么是SonarQube2、MySQL数据库的安装3、SonarQube服务端软件安…

Linux系统编程——修改配置文件(应用)

该应用主要调用到strstr函数&#xff0c;我们只需调用该函数并传入相关文件和修改数值即可&#xff0c;下面就是对strstr函数的定义解读以及实现案例 1.调用strstr函数需要包含以下头文件 #include<string.h>2.函数定义格式 char *strstr(char *str1, const char *str…

springboot苍穹外卖实战:十、缓存菜品(手动用redisTemplate实现缓存逻辑)+缓存套餐(Spring cache实现)

缓存菜品 缺点 缓存和数据库的数据一致性通常解决方案&#xff1a;延时双删、异步更新缓存、分布式锁。 该项目对于缓存菜品的处理较为简单&#xff0c;实际可以用管道技术提高redis的操作效率、同时cache自身有注解提供使用。 功能设计与缓存设计 建议这部分去看下原视频&…

吃透 Spring 系列—MVC部分

目录 ◆ SpringMVC简介 - SpringMVC概述 - SpringMVC快速入门 - Controller中访问容器中的Bean - SpringMVC关键组件浅析 ◆ SpringMVC的请求处理 - 请求映射路径的配置 - 请求数据的接收 - Javaweb常用对象获取 - 请求静态资源 - 注解驱动 标签 ◆ SpringMV…

【JUC】二、线程间的通信(虚假唤醒)

文章目录 0、多线程编程的步骤1、wait和notify2、synchronized下实现线程的通信&#xff08;唤醒&#xff09;3、虚假唤醒4、Lock下实现线程的通信&#xff08;唤醒&#xff09;5、线程间的定制化通信 0、多线程编程的步骤 步骤一&#xff1a;创建&#xff08;将来被共享的&am…

FD-Align论文阅读

FD-Align: Feature Discrimination Alignment for Fine-tuning Pre-Trained Models in Few-Shot Learning&#xff08;NeurIPS 2023&#xff09; 主要工作是针对微调的和之前的prompt tuining&#xff0c;adapter系列对比 Motivation&#xff1a; 通过模型对虚假关联性的鲁棒…

联想小新Pro14默认设置的问题

联想小新Pro14 锐龙版&#xff0c;Win11真的挺多不习惯的&#xff0c;默认配置都不符合一般使用习惯。 1、默认人走过自动开机。人机互动太强了&#xff1b; 2、默认短超时息屏但不锁屏&#xff0c;这体验很容易觉得卡机然后唤起&#xff0c;却又不用密码打开&#xff1b; 3…

(头哥)多表查询与子查询

目录 第1关&#xff1a;查询每个学生的选修的课程信息 第2关&#xff1a;查询选修了“数据结构”课程的学生名单 第3关&#xff1a;查询“数据结构”课程的学生成绩单 第4关&#xff1a;查询每门课程的选课人数 第5关&#xff1a;查询没有选课的学生信息 第6关&#xff1a…

Linux下C++调用python脚本实现LDAP协议通过TNLM认证连接到AD服务器

1.前言 首先要实现这个功能&#xff0c;必须先搞懂如何通过C调用python脚本文件最为关键&#xff0c;因为两者的环境不同。本质上是在 c 中启动了一个 python 解释器&#xff0c;由解释器对 python 相关的代码进行执行&#xff0c;执行完毕后释放资源。 2 模块功能 2.1python…

设计模式1

![在这里插入图片描述](https://img-blog.csdnimg.cn/c9fbecf1ae89436095885722380ea460.png)一、设计模式分类&#xff1a; 1、创建型模式&#xff1a;创建与使用分离&#xff0c;单例、原型、工厂、抽象、建造者。 2、结构型模式&#xff1a;用于描述如何将对象按某种更大的…

01-Spring中的工厂模式

工厂模式 工厂模式的三种形态: 工厂模式是解决对象创建问题的属于创建型设计模式,Spring框架底层使用了大量的工厂模式 第一种&#xff1a;简单工厂模式是工厂方法模式的一种特殊实现,简单工厂模式又叫静态工厂方法模式不属于23种设计模式之一第二种&#xff1a;工厂方法模式…

Leetcode—234.回文链表【简单】

2023每日刷题&#xff08;二十七&#xff09; Leetcode—234.回文链表 直接法实现代码 /*** Definition for singly-linked list.* struct ListNode {* int val;* struct ListNode *next;* };*/ bool isPalindrome(struct ListNode* head) {if(head NULL) {return t…

【mysql】CommunicationsException: Communications link failure

CommunicationsException: Communications link failure The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server. 通信异常&#xff1a;通信链路故障 最后一个成功发送到服务器的数据包是0毫秒前…

华为ensp:ospf末梢stub完全末梢totally Stub

现在宣告都宣告完了&#xff0c;现在要给area1做完全末梢 末梢区域 进入r2系统视图模式 ospf 1area 1 stub quit进入r1系统视图 ospf 1 area 1 stub quit 现在去r1上查看 末梢成功 完全末梢 进入r2系统视图 ospf 1 area 1stub no-summary 现在就成为完全末梢了&…