Java 21虚拟线程报RejectedExecutionException问题求助
问题根源
你以为虚拟线程完全独立于ForkJoinPool,但实际情况是:虚拟线程默认使用ForkJoinPool.commonPool()作为调度器。当虚拟线程中出现**同步阻塞调用(比如你代码里MinIO内部的CompletableFuture.get())**时,ForkJoinPool会启动「补偿线程」机制——虚拟线程阻塞时,原本绑定的平台线程会被释放去处理其他任务,但如果阻塞的是ForkJoinPool内部的任务(比如MinIO异步操作的回调跑在commonPool里),ForkJoinPool会尝试创建新的平台线程来补偿,避免线程池空转。
而ForkJoinPool.commonPool()的默认线程数等于CPU核心数(比如8核机器就是8个线程),当大量虚拟线程同时触发这种阻塞,需要的补偿线程数超过池的上限时,就会抛出Thread limit exceeded replacing blocked worker异常。
从报错栈能明确看到,问题出在io.minio.S3Base.uploadParts里调用的CompletableFuture.get(),这个同步阻塞触发了ForkJoinPool的补偿机制,而你的并发量已经超过了commonPool的线程上限。
解决办法
1. 替换同步阻塞为异步链式调用(推荐)
把MinIO操作中的CompletableFuture.get()改成异步链式处理,避免同步阻塞。比如不要直接调用get()等待结果,而是用thenAccept、thenApply等方法延续异步流程,让虚拟线程在IO等待时真正挂起,不需要ForkJoinPool创建补偿线程。
修改你的uploadAndDelete方法,将同步等待逻辑改为异步回调,或者直接使用MinIO提供的异步API(如果有),确保整个流程是非阻塞的。
2. 自定义调度器,避开commonPool
如果暂时无法修改MinIO的同步调用逻辑,可以给虚拟线程指定一个自定义的调度器,设置足够大的线程上限:
// 自定义ForkJoinPool,根据并发需求调整线程数 ForkJoinPool customPool = new ForkJoinPool(64); customPool.submit(() -> Thread.startVirtualThread(() -> minioUtil.uploadAndDelete(originPath, targetPath, bucketName)));
或者用专门的虚拟线程执行器,它不会依赖commonPool:
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); executor.submit(() -> minioUtil.uploadAndDelete(originPath, targetPath, bucketName));
3. 调整commonPool参数(不推荐)
可以通过JVM参数修改commonPool的线程上限,但这会影响整个应用中所有使用commonPool的代码,可能引发其他问题,谨慎使用:
-Djava.util.concurrent.ForkJoinPool.common.parallelism=64
关键提醒
虚拟线程的优势是在非阻塞IO场景下自动挂起,不占用平台线程。但如果代码里有大量同步阻塞调用(尤其是依赖ForkJoinPool的阻塞),依然会触发平台线程的限制。核心是尽量让虚拟线程中的IO操作保持非阻塞,或者用合适的调度器承载阻塞场景。
内容的提问来源于stack exchange,提问作者bebecaca

