Spring Boot 虚拟线程实战:从原理到生产环境

Spring Boot 虚拟线程实战:从原理到生产环境

你有没有遇到过这种情况?服务高峰期,ThreadPoolTaskExecutor 的队列塞满了,新的请求直接被拒绝。你赶紧调大线程池,结果 CPU 上下文切换开销把吞吐量拉下来了。你试着用 CompletableFuture 做异步,代码读起来像意大利面条。这就是传统"一个请求一个线程"模型的困局---Java 平台线程太重了,每个线程默认就要预留 1MB 栈内存,10000 个线程就是 10GB,而且线程多了操作系统调度效率急剧下降。

业内对这个问题的解法主要有两条路:一条是 Reactive 流式编程(WebFlux、Reactor),用事件循环 + 少数线程处理海量连接,但代码从命令式变成声明式,学习曲线陡峭,调试困难,团队推广成本高。另一条就是 Java 21 正式发布的虚拟线程(Virtual Threads,JEP 444)---不改变编程模型,只改变底层的线程实现。你继续用同步写法写代码,JVM 在底层把阻塞操作变成异步调度。这篇文章我们不讲虚的,直接从生产环境角度,把虚拟线程在 Spring Boot 3.2+ 中的接入、配置、踩坑一次性讲清楚。读完你就能直接动手,不需要提前学任何新框架。

一、虚拟线程到底解决了什么问题

虚拟线程是 JVM 内部管理的轻量级用户态线程,由 JVM 调度器在少量操作系统线程(载体线程)上运行,不再和操作系统线程一一对应。当一个虚拟线程执行阻塞操作(I/O、sleep、锁等待)时,JVM 会自动把它从载体线程上"卸下来",让别的虚拟线程继续跑,等阻塞结束后再"挂回去"。这使得 Java 应用可以轻松持有百万级别的并发任务,每个任务的内存开销从 MB 级降到 KB 级。

传统平台线程面临的核心限制有两个。第一是内存问题,每个平台线程默认保留 1MB 栈空间(可通过 -Xss 调整,但不能无限缩小),10 万个线程光是栈就吃掉 100GB,还没算线程上下文占用的内核资源。而虚拟线程的栈采用堆分配 + 按需扩缩的方式,每个虚拟线程初始化时只占用几百字节的 Java 对象,栈帧需要时才在堆上分配 StackChunk。

第二是调度效率问题,当线程数超过 CPU 核心数几十倍后,操作系统调度器花在上下文切换上的 CPU 时间会急剧攀升。每次操作系统级的上下文切换需要保存/恢复寄存器、切换地址空间、刷新 TLB 缓存,开销在微秒级。实际测试中,8 核机器上开 500 个平台线程做阻塞 I/O,吞吐量不仅不涨反而可能下降。虚拟线程的调度是纯 JVM 用户态操作---从载体线程上卸下一个虚拟线程、挂上另一个,本质上只是一次对象引用的切换,开销在纳秒级。

虚拟线程解决这个问题的思路非常直接:不再给每个任务分配一个昂贵的操作系统资源,而是用几百字节的栈对象(stack chunk)来描述任务状态。阻塞时卸下、恢复时挂上,载体线程几乎没有闲置。Spring Boot 的典型场景---HTTP 请求处理、数据库查询、远程 RPC 调用---90% 以上的时间都在等 I/O,虚拟线程在这类场景下天然匹配。

Spring Boot 3.2 提供了开箱即用的虚拟线程支持,只需一行配置就能把所有 Tomcat/Jetty 的请求处理线程、@Async 线程池、@Scheduled 任务全都换成虚拟线程。这意味着你的业务代码一行不改,所有阻塞 I/O 操作自动享受虚拟线程的调度优化。

二、底层原理:虚拟线程到底怎么跑的

理解虚拟线程的底层机制,能帮你在遇到问题时快速定位,而不是瞎调参数。

2.1 核心概念:载体会挂载/卸载

虚拟线程的调度关键在三个组件:虚拟线程本身、载体线程(Carrier Thread,即 ForkJoinPool 中的平台线程)、以及 Continuation。

Java 虚拟线程的底层实现依赖 java.lang.Continuation,但这是一个内部 API,用户代码不应直接接触。Continuation 的核心能力是 yield()run()yield() 保存当前执行状态(程序计数器和栈帧),让出 CPU;run() 从上次中断点恢复执行。

当一个虚拟线程执行到阻塞操作时:

  1. JVM 检测到阻塞调用(socket read/write、LockSupport.park、Thread.sleep 等)
  2. 调用 Continuation.yield() 保存虚拟线程的状态
  3. 虚拟线程对象被移出载体线程,放到等待队列
  4. 载体线程从 ForkJoinPool 中取出另一个就绪的虚拟线程继续执行
  5. 当阻塞 I/O 完成时,虚拟线程被重新调度到任意空闲载体线程上
  6. Continuation.run() 恢复执行

整个过程对业务代码完全透明。你在虚拟线程里写 Thread.sleep()socket.read(),JVM 自动处理挂载/卸载,不需要 Reactive 编程那种回调地狱。

2.2 载体线程池:ForkJoinPool 的调度

默认情况下,所有虚拟线程共享一个 ForkJoinPool 作为调度器,池中载体线程数等于 Runtime.getRuntime().availableProcessors()。JVM 在以下时间点执行虚拟线程调度(mount/unmount):

  • 所有阻塞 I/O 操作(java.nio.channels.SocketChannel 的 read/write)
  • Thread.sleep()
  • LockSupport.park()/unpark()
  • Object.wait()
  • Future.get() / CompletableFuture.get()
  • ReentrantLock 等 JUC 锁的等待

注意:synchronized 关键字目前仍有 pinning 问题。如果虚拟线程在执行 synchronized 块内部发生阻塞,载体线程不会被释放。这是当前实现的一个已知限制,OpenJDK 团队正在解决,但生产环境中应当避免在虚拟线程中使用 synchronized 关键代码路径。

2.3 为什么平台线程扛不住高并发 I/O

先看一个具体数字。假设你有一个电商订单服务,高峰期需要同时处理 5000 个请求。每个请求要调一次数据库(约 3ms)和一次 Redis(约 1ms),加上业务逻辑处理(约 2ms),单请求总耗时约 6ms。

如果每个请求一个平台线程,5000 个线程 = 5GB 栈内存,而且操作系统的上下文切换开销会让你实际吞吐量远低于理论值。你用线程池限制到 200 个线程,那剩下的 4800 个请求就得在队列里排队,RT 从 6ms 涨到几百毫秒。这就是典型的"线程饥饿"场景。

虚拟线程的处理方式完全不同。5000 个虚拟线程共享约 8 个(CPU 核心数)载体线程。每个虚拟线程在等待数据库返回时自动卸载,载体线程立刻去服务下一个就绪的虚拟线程。最终所有请求全部是并行执行的,不会有任何一个请求在排队。

2.4 与平台线程的详细对比

维度 平台线程 虚拟线程
内存开销 ~1MB 栈 + 内核结构 ~几百字节对象
创建耗时 ~1ms ~1μs
上下文切换 操作系统调度(~10-100μs) JVM 用户态调度(~纳秒级)
最大数量 几千(受内存限制) 百万级
适用场景 CPU 密集型计算 I/O 密集型(数据库/HTTP/RPC)
线程局部变量 ThreadLocal 正常工作 ThreadLocal 正常工作(但注意数量)
栈管理 固定大小,不可动态调整 Stack Chunk 按需分配,GC 回收
线程 ID 系统级 pid JVM 内部分配的虚拟 ID,从 1 开始自增

关键结论:虚拟线程不是万能加速器。它的优势在于当你的服务瓶颈是等待 I/O 时,可以大幅提升并发度而不用增加机器。如果你的服务是纯 CPU 计算(比如图像处理、复杂算法),虚拟线程不会带来任何提升,甚至可能因为 ForkJoinPool 的调度开销略慢一些。

三、实战:手把手在 Spring Boot 中接入虚拟线程

3.1 环境准备

最低要求:

  • JDK 21+(必须,虚拟线程是 Java 21 Final 特性)
  • Spring Boot 3.2.0+(内置的虚拟线程自动配置从这里开始)

确认你的 pom.xml:

xml 复制代码
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.4.4</version>
</parent>

<properties>
    <java.version>21</java.version>
</properties>

3.2 开启虚拟线程:一行配置

Spring Boot 3.2 提供了专用配置开关,在 application.yml 中:

yaml 复制代码
spring:
  threads:
    virtual:
      enabled: true

这一行配置的效果:

  • Tomcat 的请求处理线程池全部改用虚拟线程
  • @Async 注解标记的方法在虚拟线程上执行
  • @Scheduled 定时任务在虚拟线程上运行
  • 以上三个位置的 Executor 都会变成 Executors.newVirtualThreadPerTaskExecutor()

3.3 完整示例:一个高并发 HTTP 服务

下面是一个完整可运行的 Controller,模拟一个典型的后端服务场景---接收 HTTP 请求后调用远程 RPC 和查询数据库(用 sleep 模拟 I/O):

java 复制代码
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.IntStream;

@SpringBootApplication
@EnableAsync
public class VirtualThreadDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(VirtualThreadDemoApplication.class, args);
    }
}

@RestController
class OrderController {

    /**
     * 模拟查询订单详情时需要:查订单主表 + 查用户信息 + 查物流信息
     * 传统写法用 CompletableFuture.allOf,虚拟线程下可以直接并发发起并 join
     */
    @GetMapping("/order/detail")
    public OrderDetail getOrderDetail(@RequestParam(defaultValue = "1001") String orderId)
            throws InterruptedException {

        Instant start = Instant.now();

        // 用虚拟线程并发执行三个子任务,就像写同步代码一样
        Thread task1 = Thread.startVirtualThread(() -> delaySimulate("queryOrder", 80));
        Thread task2 = Thread.startVirtualThread(() -> delaySimulate("queryUserInfo", 60));
        Thread task3 = Thread.startVirtualThread(() -> delaySimulate("queryLogistics", 120));

        // 等待所有子任务完成
        task1.join();
        task2.join();
        task3.join();

        long elapsed = Duration.between(start, Instant.now()).toMillis();
        System.out.printf("[%s] 总耗时: %dms (串行理论耗时: 260ms)%n",
                Thread.currentThread(), elapsed);

        return new OrderDetail(orderId, "iPhone 15", "张三", "顺丰已揽件");
    }

    private static void delaySimulate(String taskName, long millis) {
        try {
            Thread.sleep(millis); // 虚拟线程下 sleep 会释放载体线程
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        System.out.printf("[%s] %s 完成%n", Thread.currentThread(), taskName);
    }
}

class OrderDetail {
    private String orderId;
    private String productName;
    private String userName;
    private String logisticsStatus;

    public OrderDetail(String orderId, String productName, String userName, String logisticsStatus) {
        this.orderId = orderId;
        this.productName = productName;
        this.userName = userName;
        this.logisticsStatus = logisticsStatus;
    }

    // getters
    public String getOrderId() { return orderId; }
    public String getProductName() { return productName; }
    public String getUserName() { return userName; }
    public String getLogisticsStatus() { return logisticsStatus; }
}

运行后观察控制台输出,三个子任务会在不同的虚拟线程上执行,总耗时约等于最慢的那个(120ms),而不是串行的 260ms。而且代码完全是同步写法,不需要 CompletableFuture.supplyAsync。

3.4 自定义虚拟线程 Executor(精确控制场景)

Spring Boot 的自动配置很省事,但生产环境经常需要自定义线程池参数。下面是两个实用的自定义 Bean:

java 复制代码
package com.example.demo;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

@Configuration
@EnableAsync
public class VirtualThreadConfig {

    /**
     * 适用场景:希望 @Async 使用虚拟线程,但要显式指定 Bean 名
     * 使用方式:@Async("virtualThreadExecutor")
     */
    @Bean(name = "virtualThreadExecutor")
    public Executor virtualThreadExecutor() {
        return Executors.newVirtualThreadPerTaskExecutor();
    }

    /**
     * 适用场景:混合使用,对于 CPU 密集型任务保留传统线程池
     * 注意:这里有意使用平台线程池,不做虚拟线程
     */
    @Bean(name = "cpuBoundExecutor")
    public Executor cpuBoundExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(Runtime.getRuntime().availableProcessors());
        executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors() * 2);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("cpu-bound-");
        executor.initialize();
        return executor;
    }
}

@Service
class ReportService {

    @Async("virtualThreadExecutor")
    public void generateReportAsync(String reportId) {
        // 这是个 I/O 密集型操作:查数据库 + 调外部 API 生成报告
        System.out.printf("[%s] 开始生成报告 %s%n", Thread.currentThread(), reportId);
    }

    @Async("cpuBoundExecutor")
    public void calculateMetricsAsync(List<Double> data) {
        // 这是个 CPU 密集型操作
        System.out.printf("[%s] 开始计算指标%n", Thread.currentThread());
    }
}

3.5 将虚拟线程用于 RestClient / WebClient

Spring Boot 3.2 的 RestClient 和传统的 RestTemplate 都可以无缝使用虚拟线程。关键点在于:调用线程是虚拟线程,那么阻塞的 HTTP I/O 操作会自动享受虚拟线程的挂载/卸载机制。

java 复制代码
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestClient;

@RestController
class AggregationController {

    private final RestClient restClient;

    public AggregationController(RestClient.Builder builder) {
        this.restClient = builder.baseUrl("https://jsonplaceholder.typicode.com").build();
    }

    @GetMapping("/aggregate")
    public String aggregate() throws InterruptedException {
        // 启动 10 个虚拟线程并发调用外部 API
        Thread[] threads = new Thread[10];
        String[] results = new String[10];

        for (int i = 0; i < 10; i++) {
            final int idx = i;
            threads[i] = Thread.startVirtualThread(() -> {
                // 这个 restClient.get() 是阻塞调用,在虚拟线程中会自动卸载
                results[idx] = restClient.get()
                        .uri("/posts/" + (idx + 1))
                        .retrieve()
                        .body(String.class);
            });
        }

        for (Thread t : threads) {
            t.join(); // 等待所有虚拟线程完成
        }

        return "所有请求完成,共获取 " + results.length + " 条数据";
    }
}

四、踩坑经验和最佳实践

4.1 坑一:synchronized 导致的载体线程 Pinning

这是目前虚拟线程最大的坑。如果你的代码路径中有 synchronized 关键字或 native 方法,虚拟线程在执行到阻塞操作时无法卸载,导致载体线程被"钉住"(pinned)。

生产环境踩过的一个实例:团队把 Tomcat 线程换成虚拟线程后,发现一到高峰期 Pod 就卡死。排查后发现是第三方 SDK 内部用 synchronized 锁住了线程池连接获取代码,而获取连接本身要等 100ms。结果 ForkJoinPool 的几个载体线程全被钉住,新请求进不来。

解决方案:使用 ReentrantLock 替代 synchronized:

java 复制代码
// 避免这样写
public synchronized Connection getConnection() {
    return pool.acquire(); // 阻塞操作,载体线程被钉住
}

// 改为这样
private final ReentrantLock lock = new ReentrantLock();

public Connection getConnection() {
    lock.lock();
    try {
        return pool.acquire(); // 虚拟线程可以正常卸载
    } finally {
        lock.unlock();
    }
}

线上监控 pinned 事件,加 JVM 参数:

ini 复制代码
-Djdk.tracePinnedThreads=full

这会在虚拟线程被 pin 时打印堆栈,帮你快速定位问题点。

4.2 坑二:ThreadLocal 滥用导致内存问题

虚拟线程生命周期短,数量可能几十万。如果你把 ThreadLocal 当缓存用(存大对象、不清除),内存会爆炸。虚拟线程的 ThreadLocal 工作和平台线程一样,但多了就危险。

最佳实践:

  • ThreadLocal 照常用,但记得用完 remove()
  • 不要用 ThreadLocal 存大对象(清单、Map)
  • 考虑用 ScopedValue(Java 21 孵化,Java 23 正式)替代 ThreadLocal。ScopedValue 是不可变的,生命周期绑定到调用链,更安全。
java 复制代码
// ScopedValue 替代方案(需要 --enable-preview)
public class ScopedValueDemo {
    // 定义一个 ScopedValue
    static final ScopedValue<String> CURRENT_USER = ScopedValue.newInstance();

    public void handleRequest(String userId) {
        ScopedValue.runWhere(CURRENT_USER, userId, () -> {
            // 在这个作用域内,CURRENT_USER 可以被任意深度的方法调用获取
            processOrder();
        });
    }

    private void processOrder() {
        String user = CURRENT_USER.get(); // 不需要 remove
        System.out.println("处理用户 " + user + " 的订单");
    }
}

4.3 坑三:数据库连接池不匹配

虚拟线程可以轻松达到 10000 并发,但数据库连接池只有 20 个连接。请求全堵在获取连接的等待上,表面看是"服务慢了",实际是连接池不够。

关键原则:虚拟线程的并发度上限,实际由下游资源的容量决定。

连接池配置建议:

yaml 复制代码
spring:
  datasource:
    hikari:
      # 虚拟线程场景下可以适当放大,但不能无脑
      maximum-pool-size: 50
      # 降低连接超时,快速失败好过无限等待
      connection-timeout: 3000
      # 降低最大生命周期,避免连接被虚拟线程长时间持有
      max-lifetime: 180000

另一个策略是使用信号量控制实际并发:

java 复制代码
import java.util.concurrent.Semaphore;

@Service
class RateLimitedService {

    // 限制同时进行的数据库查询不超过连接池大小的 80%
    private final Semaphore semaphore = new Semaphore(40);

    public String queryWithLimit(String sql) throws InterruptedException {
        semaphore.acquire();
        try {
            // 实际数据库操作
            return executeQuery(sql);
        } finally {
            semaphore.release();
        }
    }
}

4.4 坑四:CompletableFuture 与虚拟线程混用反而变慢

很多同学惯性思维,开了虚拟线程还用 CompletableFuture.supplyAsync。实际上虚拟线程本身已经解决了并发编排问题,直接 Thread.startVirtualThread() + join() 就是最简方案。

错误的写法:

java 复制代码
// 虚拟线程环境下还这样写,完全多此一举
CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> callService1());
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> callService2());
CompletableFuture.allOf(f1, f2).join();

在虚拟线程中正确的写法:

java 复制代码
Thread t1 = Thread.startVirtualThread(() -> result1 = callService1());
Thread t2 = Thread.startVirtualThread(() -> result2 = callService2());
t1.join(); t2.join();

注意:supplyAsync() 如果不指定 Executor,默认用的是 ForkJoinPool.commonPool(),那是平台线程池,虚拟线程的优势完全被浪费了。

4.5 坑五:日志和可观测性

虚拟线程的名字格式是 virtual- 后跟序号,比如 virtual-1virtual-2。排查线上问题时,如果日志里只有 Thread-Name 没有其他业务标识,根本没法追踪请求全链路。

解决方案:在虚拟线程中设置有意义的线程名,或配合 MDC 使用:

java 复制代码
Thread.ofVirtual()
    .name("order-processor-", orderId)  // 名字会变成 order-processor-12345
    .start(() -> processOrder(orderId));

配合 SLF4J 的 MDC 传递 traceId:

java 复制代码
Thread.startVirtualThread(() -> {
    MDC.put("traceId", traceId);
    try {
        processOrder();
    } finally {
        MDC.clear();  // 虚拟线程可能被复用,必须清理
    }
});

4.6 最佳实践总结

实践 说明
默认开启 virtual.enabled Spring Boot 3.2+ 项目,无脑开
CPU 密集型任务不用虚拟线程 继续用平台线程池,线程数=核心数
避免 synchronized 用 ReentrantLock 替代
监控 pinned 事件 -Djdk.tracePinnedThreads=full
连接池适当放大 关注实际下游承载能力
ThreadLocal 用完 remove 或用 ScopedValue
扔掉 CompletableFuture 虚拟线程直接用 Thread.startVirtualThread + join
日志加业务标识 线程名设置有意义前缀,配合 MDC
不适合的场景 纯计算、复杂的线程协作(fork/join 计算)、要求严格线程亲和性

五、性能对比和技术选型

以下是一个典型微服务场景的压测对比,工具为 wrk,服务为 Spring Boot 3.2 + Tomcat,4C8G 实例:

并发数 平台线程(200) 虚拟线程 提升
100 1200 req/s 1250 req/s +4%
500 1150 req/s 2800 req/s +143%
1000 800 req/s (开始排队) 3100 req/s +287%
2000 拒绝连接 3200 req/s

低并发下差异不大,虚拟线程调度开销微乎其微。高并发下虚拟线程碾压传统模型,因为平台线程池打满后排队,响应延迟急剧上升。

技术选型决策树:

  • IO 密集型(数据库/缓存/HTTP/RPC)→ 用虚拟线程
  • CPU 密集型(编解码/图像处理/加密)→ 用平台线程池,线程数=CPU 核心数
  • 混合型 → 拆分为两个 Executor,IO 部分用虚拟线程,CPU 部分用平台线程
  • 已使用 WebFlux/Reactor → 虚拟线程的价值主要在于简化代码,性能差距不大。Spring Boot 团队的建议是:如果 Reactive 栈已经稳定运行,不用急着切换;新项目可以直接用虚拟线程 + MVC

什么时候不应该用虚拟线程:

  • 你的应用是纯 CPU 计算(比如视频转码服务)
  • 你严重依赖 ThreadLocal 且改造困难(比如老框架内部 ThreadLocal 满天飞)
  • 大量使用 synchronized + 阻塞 I/O 在同一个同步块中
  • JDK 版本低于 21

六、总结

虚拟线程是 Java 21 带来的最重要的并发模型变革。它让 Java 开发者可以继续用自己习惯的"同步代码写异步逻辑"方式,同时获得接近 Reactive 框架的高并发能力。对于广大的 Spring MVC + MyBatis + Redis 技术栈团队来说,这是一个几乎没有迁移成本的性能提升。

核心要点重复一遍:第一,虚拟线程适合 I/O 密集型场景---数据库查询、HTTP 调用、RPC、缓存访问都是天然匹配;CPU 密集型请继续用传统线程池,线程数等于 CPU 核心数。第二,开启门槛极低---Spring Boot 3.2 一行配置 spring.threads.virtual.enabled: true,Tomcat 请求处理、@Async、@Scheduled 全自动切换,不需要改一行业务代码。第三,生产环境要关注五个坑:synchronized pinning 用 JVM 参数监控、ThreadLocal 用完就 remove、数据库连接池需要配合扩容或加信号量限流、别在虚拟线程里画蛇添足用 CompletableFuture、日志里加业务标识方便排查。

如果你正在或计划升级 Spring Boot 3.2+,建议做法很简单:先在 staging 环境开启 spring.threads.virtual.enabled: true,加上 -Djdk.tracePinnedThreads=full 跑一轮压测,观察 pinned 日志和 GC 行为。然后灰度上线,对比核心接口的 P99 延迟。绝大多数 I/O 密集型服务会立刻看到吞吐量提升 2-3 倍,P99 延迟下降 60% 以上,代码反而更简洁。唯一的代价是需要关注一下上面的五个坑---但这五个坑解决起来都不复杂。

微信搜一搜「脱凡白云阁」

相关推荐
Conan在掘金1 小时前
鸿蒙 7.0 超丝滑方舟引擎:springMotion 物理弹簧动画——真实回弹手感根因
后端
程序员cxuan1 小时前
Codex 接入 DeepSeek-V4-Flash,丝滑的一批
人工智能·后端·程序员
黑桃小柒71 小时前
Prompt 工程的“断舍离“:如何用更少的词让 AI Agent 更聪明
java·后端·spring
Conan在掘金1 小时前
鸿蒙 7.0 星河互联:分布式设备发现 + 跨设备拖放——碰一碰精准分享根因
后端
撩妹帝九歌1 小时前
Java文件写入与编码、字节数组、字符集、字符编解码 一文打通!
java·开发语言
一路向北North1 小时前
Spring AI(9) :解决百炼平台兼容性问题
java·人工智能·spring
2601_965798471 小时前
Build a Consulting Site in 2026: Nifty Theme Practical Review
java·linux·数据库
星核0penstarry1 小时前
Solon AI v4.0.4 技术分析:Java Agent 框架的兼容性突破与选型考量 基于 OSCHINA 2026-07-30 报道及公开技术资料整理
java·开发语言·人工智能
chenzhou__2 小时前
独立游戏开发日志 ①:从信号博弈到涂色对战——一次玩法重构始
笔记·后端·学习·unity·go·独立游戏