JDK21虚拟线程

news2025/1/21 22:05:43

目录

虚拟线程

话题

什么是平台线程?

什么是虚拟线程?

为什么要使用虚拟线程?

创建和运行虚拟线程

使用线程类和线程创建虚拟线程。生成器界面

使用Executor.newVirtualThreadPerTaskExecutor()方法创建和运行虚拟线程

调度虚拟线程和固定虚拟线程

调试虚拟线程

JDK虚拟线程的飞行记录器事件


官方文档的翻译版本

官方文档:Virtual Threads

虚拟线程

虚拟线程是轻量级线程,可以减少编写、维护和调试高吞吐量并发应用程序的工作量。

有关虚拟线程的背景信息,请参阅JEP444。

线程是可以调度的最小处理单元。它与其他此类单元同时运行,并且在很大程度上独立于其他此类单元。它是java.lang.Thread的一个实例。有两种线程,平台线程和虚拟线程。

话题

  • 什么是平台线程?
  • 什么是虚拟线程?
  • 为什么要使用虚拟线程?
  • 创建和运行虚拟线程
  • 调度虚拟线程和固定虚拟线程
  • 调试虚拟线程
  • 虚拟线程:采用指南

什么是平台线程?

平台线程被实现为操作系统(OS)线程周围的瘦包装器。平台线程在其底层操作系统线程上运行Java代码,平台线程在平台线程的整个生命周期中捕获其操作系统线程。因此,可用平台线程的数量被限制为OS线程的数量。

平台线程通常具有由操作系统维护的大型线程堆栈和其他资源。它们适用于运行所有类型的任务,但可能是有限的资源。

什么是虚拟线程?

与平台线程一样,虚拟线程也是java.lang.thread的一个实例。然而,虚拟线程并没有绑定到特定的操作系统线程。虚拟线程仍然在操作系统线程上运行代码。但是,当虚拟线程中运行的代码调用阻塞I/O操作时,Java运行时会挂起虚拟线程,直到可以恢复为止。与挂起的虚拟线程相关联的OS线程现在可以自由地执行其他虚拟线程的操作。

虚拟线程的实现方式与虚拟内存类似。为了模拟大量内存,操作系统将大量虚拟地址空间映射到有限的RAM。同样,为了模拟大量线程,Java运行时将大量虚拟线程映射到少量操作系统线程。

与平台线程不同,虚拟线程通常有一个浅调用堆栈,只执行一个HTTP客户端调用或一个JDBC查询。尽管虚拟线程支持线程本地变量和可继承的线程本地变量,但您应该仔细考虑使用它们,因为单个JVM可能支持数百万个虚拟线程。

虚拟线程适用于运行大部分时间被阻塞的任务,这些任务通常等待I/O操作完成。然而,它们并不适用于长时间运行的CPU密集型操作。

为什么要使用虚拟线程?

在高吞吐量并发应用程序中使用虚拟线程,尤其是那些由大量并发任务组成、花费大量时间等待的应用程序。服务器应用程序是高吞吐量应用程序的示例,因为它们通常处理许多执行阻塞I/O操作(如获取资源)的客户端请求。

虚拟线程不是更快的线程;它们运行代码的速度并不比平台线程快。它们的存在是为了提供规模(更高的吞吐量),而不是速度(更低的延迟)。

创建和运行虚拟线程

线程和线程。生成器API提供了创建平台线程和虚拟线程的方法。java.util.concurrent。Executors类还定义了创建ExecutorService的方法,该方法为每个任务启动一个新的虚拟线程。

话题

  • 使用线程类和线程创建虚拟线程。生成器界面
  • 使用Executor.newVirtualThreadPerTaskExecutor()方法创建和运行虚拟线程
  • 多线程客户端服务器示例

使用线程类和线程创建虚拟线程。生成器界面

调用Thread.ofVirtual()方法来创建Thread的实例。用于创建虚拟线程的生成器。

以下示例创建并启动一个打印消息的虚拟线程。它调用联接方法以等待虚拟线程终止。(这使您能够在主线程终止之前看到打印的消息。)

Thread thread = Thread.ofVirtual().start(() -> System.out.println("Hello"));
thread.join();

生成器界面允许您创建具有通用线程属性(如线程名称)的线程。线程。建设者OfPlatform子接口创建平台线程,而Thread。建设者OfVirtual创建虚拟线程。

以下示例使用thread创建一个名为MyThread的虚拟线程。生成器界面:

Thread.Builder builder = Thread.ofVirtual().name("MyThread");
Runnable task = () -> {
    System.out.println("Running thread");
};
Thread t = builder.start(task);
System.out.println("Thread t name: " + t.getName());
t.join();

以下示例使用Thread创建并启动两个 Thread.Builder

Thread.Builder builder = Thread.ofVirtual().name("worker-", 0);
Runnable task = () -> {
    System.out.println("Thread ID: " + Thread.currentThread().threadId());
};

// name "worker-0"
Thread t1 = builder.start(task);   
t1.join();
System.out.println(t1.getName() + " terminated");

// name "worker-1"
Thread t2 = builder.start(task);   
t2.join();  
System.out.println(t2.getName() + " terminated");

此示例打印类似于以下内容的输出:

Thread ID: 21
worker-0 terminated
Thread ID: 24
worker-1 terminated

使用Executor.newVirtualThreadPerTaskExecutor()方法创建和运行虚拟线程

执行器允许您将线程管理和创建与应用程序的其余部分分开。

以下示例使用Executors.newVirtualThreadPerTaskExecutor()方法创建ExecutorService。每当调用ExecutorService.submit(Runnable)时,都会创建并启动一个新的虚拟线程来运行任务。此方法返回Future的一个实例。请注意,方法Future.get()等待线程的任务完成。因此,此示例在虚拟线程的任务完成后打印一条消息。

try (ExecutorService myExecutor = Executors.newVirtualThreadPerTaskExecutor()) {
    Future<?> future = myExecutor.submit(() -> System.out.println("Running thread"));
    future.get();
    System.out.println("Task completed");
    // ...

多线程客户端服务器示例

以下示例由两个类组成。EchoServer是一个服务器程序,它侦听端口并为每个连接启动一个新的虚拟线程。EchoClient是一个连接到服务器并发送在命令行上输入的消息的客户端程序。

EchoClient创建一个套接字,从而连接到EchoServer。它在标准输入流上读取用户的输入,然后通过将文本写入套接字将文本转发到EchoServer。EchoServer通过插座将输入回波至EchoClient。EchoClient读取并显示从服务器传回的数据。EchoServer可以通过虚拟线程同时为多个客户端提供服务,每个客户端连接一个线程

public class EchoServer {
    
    public static void main(String[] args) throws IOException {
         
        if (args.length != 1) {
            System.err.println("Usage: java EchoServer <port>");
            System.exit(1);
        }
         
        int portNumber = Integer.parseInt(args[0]);
        try (
            ServerSocket serverSocket =
                new ServerSocket(Integer.parseInt(args[0]));
        ) {                
            while (true) {
                Socket clientSocket = serverSocket.accept();
                // Accept incoming connections
                // Start a service thread
                Thread.ofVirtual().start(() -> {
                    try (
                        PrintWriter out =
                            new PrintWriter(clientSocket.getOutputStream(), true);
                        BufferedReader in = new BufferedReader(
                            new InputStreamReader(clientSocket.getInputStream()));
                    ) {
                        String inputLine;
                        while ((inputLine = in.readLine()) != null) {
                            System.out.println(inputLine);
                            out.println(inputLine);
                        }
                    
                    } catch (IOException e) { 
                        e.printStackTrace();
                    }
                });
            }
        } catch (IOException e) {
            System.out.println("Exception caught when trying to listen on port "
                + portNumber + " or listening for a connection");
            System.out.println(e.getMessage());
        }
    }
}
public class EchoClient {
    public static void main(String[] args) throws IOException {
        if (args.length != 2) {
            System.err.println(
                "Usage: java EchoClient <hostname> <port>");
            System.exit(1);
        }
        String hostName = args[0];
        int portNumber = Integer.parseInt(args[1]);
        try (
            Socket echoSocket = new Socket(hostName, portNumber);
            PrintWriter out =
                new PrintWriter(echoSocket.getOutputStream(), true);
            BufferedReader in =
                new BufferedReader(
                    new InputStreamReader(echoSocket.getInputStream()));
        ) {
            BufferedReader stdIn =
                new BufferedReader(
                    new InputStreamReader(System.in));
            String userInput;
            while ((userInput = stdIn.readLine()) != null) {
                out.println(userInput);
                System.out.println("echo: " + in.readLine());
                if (userInput.equals("bye")) break;
            }
        } catch (UnknownHostException e) {
            System.err.println("Don't know about host " + hostName);
            System.exit(1);
        } catch (IOException e) {
            System.err.println("Couldn't get I/O for the connection to " +
                hostName);
            System.exit(1);
        } 
    }
}

调度虚拟线程和固定虚拟线程

操作系统安排平台线程何时运行。但是,Java运行时会安排虚拟线程何时运行。当Java运行时调度虚拟线程时,它将虚拟线程分配或装载到平台线程上,然后操作系统照常调度该平台线程。这个平台线程被称为载体。在运行一些代码后,虚拟线程可以从其承载器中卸载。这种情况通常发生在虚拟线程执行阻塞I/O操作时。虚拟线程从其载体上卸载后,载体是空闲的,这意味着Java运行时调度程序可以在其上装载不同的虚拟线程。

当虚拟线程固定在其承载器上时,在阻塞操作期间无法卸载它。虚拟线程在以下情况下被固定:

  • 虚拟线程在同步块或方法内运行代码
  • 虚拟线程运行本机方法或外部函数(请参阅外部函数和内存API)

固定不会使应用程序出错,但可能会阻碍其可扩展性。通过修改频繁运行的同步块或方法,并使用java.util.concurrent.locks保护潜在的长I/O操作,尝试避免频繁和长期的固定。重新输入锁定。

调试虚拟线程

虚拟线程是静态线程;调试器可以像平台线程一样逐步完成它们。JDK Flight Recorder和jcmd工具具有其他功能,可以帮助您观察应用程序中的虚拟线程。

话题

  • JDK虚拟线程的飞行记录器事件
  • 查看jcmd线程转储中的虚拟线程

JDK虚拟线程的飞行记录器事件

JDK飞行记录器(JFR)可以发出以下与虚拟线程相关的事件:

  • jdk.VirtualThreadStart和jdk。VirtualThreadEnd指示虚拟线程何时开始和结束。默认情况下,这些事件处于禁用状态。
  • dk.VirtualThreadPinned 表示虚拟线程被固定(其承载线程未被释放)的时间超过阈值持续时间。此事件默认启用,阈值为20ms。
  • jdk.VirtualThreadSubmitFailed表示启动或取消标记虚拟线程失败,可能是由于资源问题。停放虚拟线程会释放底层承载线程来执行其他工作,而取消标记虚拟线程则会调度它继续执行。默认情况下会启用此事件。

使用按请求线程样式的阻塞I/O API编写简单的同步代码

虚拟线程可以显著提高以每请求线程方式编写的服务器的吞吐量,而不是延迟。在这种风格中,服务器将一个线程专门用于处理每个传入请求的整个持续时间。它至少专用一个线程,因为在处理单个请求时,您可能希望使用更多的线程来同时执行一些任务。

阻塞一个平台线程是昂贵的,因为它保留了线程——一种相对稀缺的资源——而它没有做太多有意义的工作。因为虚拟线程可能很多,所以阻塞它们是廉价的,也是受鼓励的。因此,您应该以直接的同步风格编写代码,并使用阻塞I/O API。

例如,以下代码以非阻塞异步风格编写,不会从虚拟线程中获得太多好处。

CompletableFuture.supplyAsync(info::getUrl, pool)
   .thenCompose(url -> getBodyAsync(url, HttpResponse.BodyHandlers.ofString()))
   .thenApply(info::findImage)
   .thenCompose(url -> getBodyAsync(url, HttpResponse.BodyHandlers.ofByteArray()))
   .thenApply(info::setImageData)
   .thenAccept(this::process)
   .exceptionally(t -> { t.printStackTrace(); return null; });

另一方面,以下代码以同步风格编写,并使用简单的阻塞IO,将受益匪浅:

try {
   String page = getBody(info.getUrl(), HttpResponse.BodyHandlers.ofString());
   String imageUrl = info.findImage(page);
   byte[] data = getBody(imageUrl, HttpResponse.BodyHandlers.ofByteArray());   
   info.setImageData(data);
   process(info);
} catch (Exception ex) {
   t.printStackTrace();
}

这样的代码也更容易在调试器中调试、在探查器中配置文件或使用线程转储进行观察。要观察虚拟线程,请使用jcmd命令创建一个线程转储:

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

以这种风格编写的堆栈越多,虚拟线程的性能和可观察性就越好。以其他风格编写的程序或框架,如果不为每个任务指定一个线程,就不应该期望从虚拟线程中获得显著的好处。避免将同步、阻塞代码与异步框架混合使用。

将每个并发任务表示为一个虚拟线程;从不池化虚拟线程

关于虚拟线程,最难内化的是,尽管它们与平台线程具有相同的行为,但它们不应该表示相同的程序概念。

平台线程是稀缺的,因此是一种宝贵的资源。需要管理宝贵的资源,而管理平台线程的最常见方式是使用线程池。然后您需要回答的一个问题是,池中应该有多少个线程?

但虚拟线程是丰富的,因此每个线程都不应该代表一些共享的、池化的资源,而是一个任务。线程从托管资源变成应用程序域对象。我们应该有多少虚拟线程的问题变得显而易见,就像我们应该使用多少字符串在内存中存储一组用户名的问题一样:虚拟线程的数量总是等于应用程序中并发任务的数量。

将n个平台线程转换为n个虚拟线程几乎没有好处;相反,需要转换的是任务。

要将每个应用程序任务表示为线程,请不要像下面的示例那样使用共享线程池执行器:

Future<ResultA> f1 = sharedThreadPoolExecutor.submit(task1);
Future<ResultB> f2 = sharedThreadPoolExecutor.submit(task2);
// ... use futures

相反,使用虚拟线程执行器,如以下示例所示:

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
   Future<ResultA> f1 = executor.submit(task1);
   Future<ResultB> f2 = executor.submit(task2);
   // ... use futures
}

代码仍然使用ExecutorService,但从Executors.newVirtualThreadPerTaskExecutor()返回的代码没有使用线程池。相反,它为每个提交的任务创建一个新的虚拟线程。

此外,ExecutorService本身是轻量级的,我们可以创建一个新的,就像处理任何简单的对象一样。这使我们能够依赖于新添加的ExecutorService.close()方法和try-with-resources构造。在try块结束时隐式调用的close方法将自动等待提交给ExecutorService的所有任务(即ExecutorServices派生的所有虚拟线程)终止。

对于输出场景,这是一个特别有用的模式,在输出场景中,您希望同时执行对不同服务的多个传出调用,如以下示例所示:

void handle(Request request, Response response) {
    var url1 = ...
    var url2 = ...
 
    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);
    }
}
 
String fetchURL(URL url) throws IOException {
    try (var in = url.openStream()) {
        return new String(in.readAllBytes(), StandardCharsets.UTF_8);
    }
}

您应该创建一个新的虚拟线程,如上所示,用于即使是小的、短暂的并发任务。

为了获得更多关于编写输出模式和其他具有更好可观察性的常见并发模式的帮助,请使用结构化并发。

根据经验,如果您的应用程序从来没有10000个或更多的虚拟线程,那么它就不太可能从虚拟线程中受益。要么它的负载太轻,需要更好的吞吐量,要么您没有向虚拟线程表示足够多的任务。

使用信号量限制并发

有时需要限制某个操作的并发性。例如,某些外部服务可能无法处理超过十个并发请求。由于平台线程是一种宝贵的资源,通常在池中进行管理,线程池变得如此普遍,以至于它们被用于限制并发性,如以下示例所示:

ExecutorService es = Executors.newFixedThreadPool(10);
...
Result foo() {
    try {
        var fut = es.submit(() -> callLimitedService());
        return f.get();
    } catch (...) { ... }
}

此示例确保对有限服务最多有十个并发请求。

但限制并发只是线程池操作的副作用。池是为了共享稀缺资源而设计的,虚拟线程并不稀缺,因此永远不应该被池化!

在使用虚拟线程时,如果您想限制访问某些服务的并发性,则应该使用专门为此目的设计的构造:Semaphore类。以下示例演示了此类:

Semaphore sem = new Semaphore(10);
...
Result foo() {
    sem.acquire();
    try {
        return callLimitedService();
    } finally {
        sem.release();
    }
}

碰巧调用foo的线程将被抑制,也就是说,被阻塞,这样一次只有十个线程可以取得进展,而其他线程将不受阻碍地进行业务。

简单地用信号量阻塞一些虚拟线程似乎与将任务提交到固定线程池有很大不同,但事实并非如此。将任务提交给线程池会使它们排队等待稍后执行,但信号量在内部(或任何其他阻塞同步结构)创建一个被阻塞的线程队列,该队列反映了等待池线程执行它们的任务队列。由于虚拟线程是任务,因此生成的结构是等效的:

尽管您可以将平台线程池视为处理从队列中提取的任务的工作线程,将虚拟线程视为任务本身,直到它们可以继续,但计算机中的底层表示实际上是相同的。识别排队任务和阻塞线程之间的等效性将有助于充分利用虚拟线程。

数据库连接池本身就是一个信号灯。限制为十个连接的连接池将阻止第十一个线程尝试获取连接。不需要在连接池的顶部添加额外的信号量。

不要在线程本地变量中缓存昂贵的可重用对象

虚拟线程和平台线程一样支持线程本地变量。有关详细信息,请参阅线程本地变量(thread local variables)。通常,线程局部变量用于将一些特定于上下文的信息与当前运行的代码相关联,例如当前事务和用户ID。这种线程局部变量的使用对于虚拟线程来说是完全合理的。但是,请考虑使用更安全、更高效的作用域值。有关详细信息,请参阅作用域值。

线程局部变量的另一种用法与虚拟线程根本不同:缓存可重用对象。这些对象的创建成本通常很高(并且会消耗大量内存),是可变的,并且不是线程安全的。它们被缓存在线程局部变量中,以减少它们被实例化的次数和内存中的实例数量,但它们会被线程上不同时间运行的多个任务重用。

例如,SimpleDateFormat的实例的创建成本很高,而且不是线程安全的。出现的一种模式是将这样的实例缓存在ThreadLocal中,如以下示例所示:

static final ThreadLocal<SimpleDateFormat> cachedFormatter = 
       ThreadLocal.withInitial(SimpleDateFormat::new);

void foo() {
  ...
	cachedFormatter.get().format(...);
	...
}

只有当线程(以及缓存在线程本地中的昂贵对象)被多个任务共享和重用时,这种缓存才有帮助,就像平台线程被池化时一样。许多任务在线程池中运行时可能会调用foo,但由于线程池只包含几个线程,因此对象只会实例化几次——每个池线程一次——缓存并重用。

然而,虚拟线程从不被池化,也从不被不相关的任务重用。因为每个任务都有自己的虚拟线程,所以来自不同任务的每次对foo的调用都会触发新SimpleDateFormat的实例化。此外,由于可能有大量虚拟线程同时运行,因此昂贵的对象可能会消耗相当多的内存。这些结果与线程本地缓存所要实现的结果正好相反。

没有单一的通用替代方案可供选择,但在SimpleDateFormat的情况下,您应该将其替换为DateTimeFormatter。DateTimeFormatter是不可变的,因此所有线程都可以共享一个实例:

static final DateTimeFormatter formatter = DateTimeFormatter….;

void foo() {
  ...
	formatter.format(...);
	...
}

请注意,使用线程局部变量缓存共享的昂贵对象有时是由异步框架在幕后完成的,因为异步框架隐含地假设它们由极少数池线程使用。这就是为什么混合虚拟线程和异步框架不是一个好主意的原因之一:对方法的调用可能会导致在线程本地变量中实例化旨在缓存和共享的代价高昂的对象。

避免长时间和频繁的固定

当前虚拟线程实现的一个限制是,在同步块或方法内部执行阻塞操作会导致JDK的虚拟线程调度程序阻塞宝贵的操作系统线程,而如果阻塞操作在同步块和方法之外执行,则不会阻塞。我们称这种情况为“钉扎”。如果阻塞操作是长期且频繁的,则固定可能会对服务器的吞吐量产生不利影响。保护短暂的操作,如内存中的操作,或具有同步块或方法的不频繁操作,应该不会产生不利影响。

为了检测可能有害的钉扎实例,(JDK飞行记录器(JFR)会发出JDK。VirtualThreadPined线程当锁定了阻塞操作时;默认情况下,当操作耗时超过20ms时,会启用此事件。

或者,可以使用系统属性jdk.tracePinnedThreads在线程被固定时阻塞时发出堆栈跟踪。使用选项Djdk.tracePinnedThreads=full运行时,当线程在固定时发生阻塞时,将打印完整的堆栈跟踪,突出显示本地帧和持有监视器的帧。使用选项Djdk.tracePinnedThreads=short运行会将输出仅限于有问题的帧。

如果这些机制检测到固定既长时间又频繁的位置,请在这些特定位置使用synchronized with ReentrantLock来替换(同样,在保护短时间或不频繁操作的位置,无需替换synchronized)。以下是同步化块的长期频繁使用示例。

synchronized(lockObj) {
    frequentIO();
}
lock.lock();
try {
    frequentIO();
} finally {
    lock.unlock();
}

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

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

相关文章

k8s详细教程

Kubernetes详细教程 1. Kubernetes介绍 1.1 应用部署方式演变 在部署应用程序的方式上&#xff0c;主要经历了三个时代&#xff1a; 传统部署&#xff1a;互联网早期&#xff0c;会直接将应用程序部署在物理机上 优点&#xff1a;简单&#xff0c;不需要其它技术的参与 缺点…

Spring Cloud Alibaba微服务从入门到进阶(三)(Spring Cloud Alibaba)

Spring Cloud Alibaba是spring Cloud的子项目 Spring Cloud Alibaba的主要组件&#xff08;红框内是开源的&#xff09; Spring Cloud是快速构建分布式系统的工具集&#xff0c; Spring Cloud提供了很多分布式功能 Spring Cloud常用子项目 项目整合 Spring Cloud Alibaba …

VUE-组件间通信(一)props

props 1、单向绑定 props是父组件给子组件传输数据 当父组件的属性变化时&#xff0c;将传导给子组件&#xff0c;但是反过来不会 2、使用示例 子组件&#xff08;类似于方法&#xff09; <template> <div><h2>姓名:{{ name }}</h2><h2>性别:{{…

微信小程序接口请求出错:request:fail url not in domain list:xxxxx

一、微信小程序后台和开发者工具配的不一样导致了这个错误 先说结论&#xff1a; 开发者工具配置了https://www.xxx.cn/prod-api/ 微信后台配置了 https://www.xxx.cn 一、最开始 开发者工具配置了https://www.xxx.cn:7500 微信后台配置了 https://www.xxx.cn 报错:reques…

面试六分钟,难题显真章

职场&#xff0c;这个充满机遇与挑战的舞台&#xff0c;总会在不经意间上演着意想不到的转折。我从一家小公司转投到另一家&#xff0c;原本期待着新的工作环境和更多的发展机会&#xff0c;然而现实却给了我一个不小的打击。 新公司的加班文化&#xff0c;如同一个巨大的漩涡…

yolov7 gui 轻松通过GUI来实现YOLOv7对象检测

YOLOv7 GUI 是一款用户友好型图形界面应用程序&#xff0c;专为简化基于YOLOv7&#xff08;You Only Look Once version 7&#xff09;的目标检测流程而设计。该工具允许用户无需深入掌握命令行操作和复杂编程细节&#xff0c;即可方便快捷地运行YOLOv7模型来检测图像或视频中的…

3.19总结

A计划 题解&#xff1a;这题其实就是一个很简单的三维搜索&#xff0c;有了一个传送门&#xff0c;并且要确定是否传过去的对应位置是墙&#xff0c;防止被装死&#xff0c;同事呢又要在对应的t时间内完成&#xff08;不一定要卡着t时间恰好完成&#xff09; #include<ios…

【项目实践day06】JWT令牌相关

什么是JWT 简洁的、自包含的格式&#xff0c;用于在通信双方以json数据格式安全的传输信息。 由于数字签名的存在&#xff0c;这些信息是可靠的。 jwt就是将原始的json数据格式进行了安全的封装&#xff0c;这样就可以直接基于jwt在通信双方安全的进行信息传输了。简洁&#…

全栈的自我修养 ———— 让uniapp开发更加舒服!!(与别的博主思路不一样,小编这里只讲实用的,直提重点!)

小编是web的&#xff0c;然后现在开始接手微信小程序&#xff0c;有很多不习惯的的地方&#xff0c;经过一段时间的使用&#xff0c;部分得到了妥善的解决方法 一、用vscode开发小程序二、组件库的选择三、注意 一、用vscode开发小程序 发现用Hbuilder开发小程序有很多不习惯的…

odoo17开发教程(8):设置界面UI的字段属性

目录 添加字段 给字段设置只读和不可拷贝 给字段添加默认值 保留字段 本节目标&#xff1a;在本文末尾&#xff0c;售价(selling price)应为只读值&#xff0c;卧室数量(bedrooms)和可用日期(availability date)应为默认值。此外&#xff0c;在复制记录时&#xff0c;售价和…

day09-Mybatis

一、Mybatis 基础操作 1 需求 功能列表&#xff1a; 查询 根据主键ID查询 条件查询新增更新删除 根据主键ID删除 根据主键ID批量删除 2 准备 实施前的准备工作&#xff1a; 准备数据库表创建一个新的 springboot 工程&#xff0c;选择引入对应的起步依赖&#xff08;mybatis、…

【DevOps趣味篇】你为什么要数程序员的代码行数?

【DevOps趣味篇】你为什么要数程序员的代码行数&#xff1f; 目录 【DevOps趣味篇】你为什么要数程序员的代码行数&#xff1f;代码行数统计方法手动计数代码行数统计命令使用语句统计代码行数IL指令 需要计算代码行数吗&#xff1f; 推荐超级课程&#xff1a; Docker快速入门…

「Swift」AttributedString常见使用方法

前言&#xff1a;AttributedString是Apple推出的可以实现单个字符或字符范围带相应属性的字符串。属性提供了一些文本特性&#xff0c;可以让文本展示的样式更加丰富。在日常开发过程中&#xff0c;我通常用于同一个Label中包含不同的字体大小或字体颜色的样式编写中。 使用举…

002——编译鸿蒙(Liteos -a)

目录 一、鸿蒙是什么 二、Kconfig 2.1 概述 2.2 编译器 2.3 make使用 本文章引用了很多韦东山老师的教程内容&#xff0c;算是我学习过程中的笔记吧。如果侵权请联系我。 一、鸿蒙是什么 这里我补充一下对鸿蒙的描述 这张图片是鸿蒙发布时使用的&#xff0c;鸿蒙是一个很…

数据预处理:重复值

数据重复值处理 数据重复值出现情况重复的记录用于分析演变规律重复的记录用于样本不均衡处理重复的记录用于检测业务规则问题 数据重复值出现情况 数据集中的重复值包括以下两种情况&#xff1a; 数据值完全相同的多条数据记录。这是最常见的数据重复情况。数据主体相同但匹…

ConKI: Contrastive Knowledge Injection for Multimodal Sentiment Analysis

文章目录 ConKI&#xff1a;用于多模态情感分析的对比知识注入文章信息研究目的研究内容研究方法1.总体结构2.Encoding with Knowledge Injection2.1 Pan-knowledge representations2.2 Knowledge-specific representations 3.Hierarchical Contrastive Learning4.损失函数5.训…

五个跟进方法,让你的老外客户不再跑路!

一、不同客户该怎么跟进? 1.已报价的客户 在向客户报过价之后&#xff0c;过几天要记得再询问一下对方是否收到了报价&#xff0c;如果没收到就提醒一下客户必要时将价格再发过去&#xff0c;如果客户已收到还要再进一步了解其对于报价的想法。 如果客户有兴趣也有需要&…

外包2月,技术倒退警钟长鸣。。。。。

曾经的我&#xff0c;作为一名大专生&#xff0c;在湖南某软件公司从事功能测试工作近四年。日复一日的工作让我陷入舒适区&#xff0c;不思进取。直到今年8月&#xff0c;我才意识到自己的技术停滞不前&#xff0c;女友的离开更是让我痛定思痛&#xff0c;决定改变现状&#x…

如何选择合适的奶瓶?五大超实用选购技巧,新手宝妈必看

奶瓶什么品牌好&#xff1f;奶瓶是每个新生宝宝都需要用到的辅喂产品&#xff0c;然而市场上许多网红品牌为了赚快钱&#xff0c;往往凭借外观设计、性价比和广告营销来吸引消费者。这些品牌由于缺乏专业技术&#xff0c;往往没有对选材用料和安全性进一步的优化&#xff0c;从…

使用jQuery的autocomplete实现数据查询一次,联想自动补全

书接上回&#xff0c;上次说到在jsp页面中&#xff0c;通过监听输入框的数值变化&#xff0c;实时查询数据库&#xff0c;得到返回值使用autocomplete属性自动补全&#xff0c;实现一个联想补全辅助操作&#xff0c;链接&#xff1a;使用jquery的autocomplete属性实现联想补全操…