Этапы воспроизведения TLDR:
- Оформить заказ https://github.com/stsypanov/concurrency-demo
- Запустить DependencyApplication и ConcurrencyDemoApplication
- Когда оба приложения заработают, запустите StuckApplicationTest.
- Завершение теста займет около 1–2 минут.
- Теперь перейдите в demo-service/application.yml и установите для Spring.threads.virtual.enabled: true (по умолчанию false).
Перезапустите ConcurrencyDemoApplication - Запустите StuckApplicationTest еще раз
- Запустите профилировщик YourKit и подключитесь к ConcurrencyDemoApplication, почти сразу вы увидите предупреждающее сообщение о возможной взаимоблокировке, а само приложение зависнет, поскольку все потоки его ForkJoinPool имеют статус Ожидание.
Общая настройка:
- Windows 11
- Intel(R) Core(TM) i7-1370P 13-го поколения
- Liberica JDK 21.0.4
- Spring Boot 3.3.2
Код: Выделить всё
org.springframework.cloud:spring-cloud-dependencies:2023.0.3
Код: Выделить всё
io.github.openfeign:feign-httpclient
Код: Выделить всё
@Test
void name() throws InterruptedException {
var restTemplate = new RestTemplate();
var latch = new CountDownLatch(1);
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 100; i++) {
executor.submit(() -> {
try {
latch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
restTemplate.getForEntity("http://localhost:8081/actuator/health", ResponseEntity.class);
});
}
latch.countDown();
boolean b = executor.awaitTermination(100, TimeUnit.SECONDS);
assertFalse(b);
}
}
Код: Выделить всё
@Component
@RequiredArgsConstructor
public class DownstreamServiceHealthIndicator implements HealthIndicator {
private final HealthClient healthClient;
@Override
public Health health() {
var response = healthClient.checkHealth();
if (response.getStatusCode().is2xxSuccessful()) {
return new Health.Builder().up().build();
}
return new Health.Builder().down().withDetails(Map.of("response", response)).build();
}
}
@FeignClient(name = "healthClient", url = "http://localhost:8087/actuator/health", configuration = InternalFeignConfiguration.class)
public interface HealthClient {
@GetMapping
ResponseEntity checkHealth();
}
public class InternalFeignConfiguration {
@Bean
public Client client() {
return new ApacheHttpClient(HttpClients.createDefault());
}
}
При запуске система с настройками по умолчанию, все в порядке. Тест занимает ~1,5 минуты, но в остальном все в порядке.
Однако при включенных виртуальных потоках Служба A зависает, и если вы подключитесь к этому, например, В профилировщике YourKit вы получите предупреждающее сообщение о потенциальной тупиковой ситуации с помощью этой трассировки стека:
Код: Выделить всё
+-----------------------------------------------------------------------------------------------------------------------------+
| Name |
+-----------------------------------------------------------------------------------------------------------------------------+
| +---Read-Updater Frozen for at least 10s |
| | +---jdk.internal.misc.Unsafe.park(boolean, long) Unsafe.java (native) |
| | +---java.util.concurrent.locks.LockSupport.park() LockSupport.java:371 |
| | +---java.util.concurrent.LinkedTransferQueue$DualNode.await(Object, long, Object, boolean) LinkedTransferQueue.java:458 |
| | +---java.util.concurrent.LinkedTransferQueue.xfer(Object, long) LinkedTransferQueue.java:613 |
| | +---java.util.concurrent.LinkedTransferQueue.take() LinkedTransferQueue.java:1257 |
| | +---sun.nio.ch.Poller.updateLoop() Poller.java:286 |
| | +---sun.nio.ch.Poller$$Lambda.0x0000024081474670.run() |
| | +---java.lang.Thread.runWith(Object, Runnable) Thread.java:1596 |
| | +---java.lang.Thread.run() Thread.java:1583 |
| | +---jdk.internal.misc.InnocuousThread.run() InnocuousThread.java:186 |
| | |
| +---spring.cloud.inetutils Frozen for at least 10s |
| | +---java.net.Inet6AddressImpl.getHostByAddr(byte[]) Inet6AddressImpl.java (native) |
| | +---java.net.InetAddress$PlatformResolver.lookupByAddress(byte[]) InetAddress.java:1225 |
| | +---java.net.InetAddress.getHostFromNameService(InetAddress, boolean) InetAddress.java:840 |
| | +---java.net.InetAddress.getHostName(boolean) InetAddress.java:782 |
| | +---java.net.InetAddress.getHostName() InetAddress.java:754 |
| | +---org.springframework.cloud.commons.util.InetUtils$$Lambda.0x0000024081187240.call() |
| | +---java.util.concurrent.FutureTask.run() FutureTask.java:317 |
| | +---java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor$Worker) ThreadPoolExecutor.java:1144 |
| | +---java.util.concurrent.ThreadPoolExecutor$Worker.run() ThreadPoolExecutor.java:642 |
| | +---java.lang.Thread.runWith(Object, Runnable) Thread.java:1596 |
| | +---java.lang.Thread.run() Thread.java:1583 |
| | |
| +---Write-Updater Frozen for at least 10s |
| +---jdk.internal.misc.Unsafe.park(boolean, long) Unsafe.java (native) |
| +---java.util.concurrent.locks.LockSupport.park() LockSupport.java:371 |
| +---java.util.concurrent.LinkedTransferQueue$DualNode.await(Object, long, Object, boolean) LinkedTransferQueue.java:458 |
| +---java.util.concurrent.LinkedTransferQueue.xfer(Object, long) LinkedTransferQueue.java:613 |
| +---java.util.concurrent.LinkedTransferQueue.take() LinkedTransferQueue.java:1257 |
| +---sun.nio.ch.Poller.updateLoop() Poller.java:286 |
| +---sun.nio.ch.Poller$$Lambda.0x0000024081474670.run() |
| +---java.lang.Thread.runWith(Object, Runnable) Thread.java:1596 |
| +---java.lang.Thread.run() Thread.java:1583 |
| +---jdk.internal.misc.InnocuousThread.run() InnocuousThread.java:186 |
+-----------------------------------------------------------------------------------------------------------------------------+
Код: Выделить всё
// class org.springframework.cloud.commons.util.InetUtils
public HostInfo convertAddress(final InetAddress address) {
HostInfo hostInfo = new HostInfo();
Future result = this.executorService.submit(address::getHostName); //
Подробнее здесь: [url]https://stackoverflow.com/questions/78790376/spring-boot-application-gets-stuck-when-virtual-threads-are-used-on-java-21[/url]