JEP 425: Virtual Threads (Preview)

JEP 425: Virtual Threads (Preview)

Summary

Java 플랫폼에 가상 스레드를 도입합니다.
가상 스레드는 처리량이 많은 동시 응용 프로그램을 작성, 유지 관리 및 관찰하는 노력을 크게 줄이는 경량 스레드입니다.
이것은 미리보기 API 입니다.

Goals

  • 간단한 요청당 스레드 스타일로 작성된 서버 애플리케이션을 거의 최적의 하드웨어 활용으로 확장할 수 있습니다.
  • API를 사용하는 기존 코드를 활성화하여 java.lang.Thread최소한의 변경으로 가상 스레드를 채택합니다.
  • 기존 JDK 도구를 사용하여 가상 스레드의 문제 해결, 디버깅 및 프로파일링을 쉽게 수행할 수 있습니다.

Non-Goals

  • 전통적인 스레드 구현을 제거하거나 가상 스레드를 사용하도록 기존 애플리케이션을 자동으로 마이그레이션하는 것이 목표가 아닙니다.
  • Java의 기본 동시성 모델을 변경하는 것이 목표가 아닙니다.
  • Java 언어나 Java 라이브러리에서 새로운 데이터 병렬 구조를 제공하는 것이 목표가 아닙니다. Stream API는 대용량 데이터 세트를 병렬로 처리하는 데 선호되는 방법입니다.

Motivation

Java 개발자는 거의 30년 동안 동시 서버 응용 프로그램의 빌딩 블록으로 스레드에 의존해 왔습니다.
모든 메서드의 모든 명령문은 스레드 내에서 실행되며 Java는 다중 스레드이므로 한 번에 여러 스레드가 실행됩니다.
스레드는 Java의 동시성 단위입니다.이러한 다른 단위와 동시에 실행되는(거의 독립적인) 순차적 코드 조각입니다.
각 스레드는 로컬 변수를 저장하고 메서드 호출을 조정하는 스택과 상황이 잘못되었을 때 컨텍스트를 제공합니다.
동일한 스레드의 메서드에서 예외가 발생하고 포착되므로 개발자는 스레드의 스택 추적을 사용하여 무슨 일이 일어났는지 알아낼 수 있습니다.
스레드는 도구의 중심 개념이기도 합니다.
디버거는 스레드 메서드의 명령문을 단계별로 실행하고 프로파일러는 여러 스레드의 동작을 시각화하여 성능을 이해하는 데 도움을 줍니다.

The thread-per-request style (요청당 스레드 스타일)

서버 응용 프로그램은 일반적으로 서로 독립적인 동시 사용자 요청을 처리하므로 응용 프로그램이 전체 기간 동안 해당 요청에 스레드를 전용하여 요청을 처리하는 것이 좋습니다.
이 요청당 스레드 스타일은 애플리케이션의 동시성 단위를 나타내기 위해 플랫폼의 동시성 단위를 사용하기 때문에 이해하기 쉽고, 프로그래밍하기 쉽고, 디버그 및 프로파일링하기 쉽습니다.

서버 응용 프로그램의 확장성은 대기 시간, 동시성 및 처리량과 관련된 Little의 법칙 에 따라 결정됩니다.
주어진 요청 처리 기간(즉, 대기 시간) 동안 응용 프로그램이 동시에 처리하는 요청 수(즉, 동시성)는 도착률(즉, 처리량)에 비례하여 증가합니다.
예를 들어 평균 대기 시간이 50ms인 애플리케이션이 10개의 요청을 동시에 처리하여 초당 200개의 요청 처리량을 달성한다고 가정합니다.
해당 애플리케이션이 초당 2000개의 요청 처리량으로 확장하려면 100개의 요청을 동시에 처리해야 합니다.
각 요청이 요청 기간 동안 스레드에서 처리되는 경우 애플리케이션이 이를 따라잡으려면 처리량이 증가함에 따라 스레드 수가 증가해야 합니다.

불행히도 사용 가능한 스레드의 수는 JDK가 운영 체제(OS) 스레드 주변의 래퍼로 스레드를 구현하기 때문에 제한됩니다.
OS 스레드는 비용이 많이 들기 때문에 스레드를 너무 많이 가질 수 없으므로 구현이 요청당 스레드 스타일에 적합하지 않습니다.
각 요청이 해당 기간 동안 스레드, 즉 OS 스레드를 사용하는 경우 CPU 또는 네트워크 연결과 같은 다른 리소스가 고갈되기 오래 전에 스레드 수가 제한 요소가 되는 경우가 많습니다.
JDK의 현재 스레드 구현은 애플리케이션의 처리량을 하드웨어가 지원할 수 있는 것보다 훨씬 낮은 수준으로 제한합니다.
이는 스레드가 풀링된 경우에도 발생합니다. 풀링은 새 스레드를 시작하는 높은 비용을 방지하는 데 도움이 되지만 총 스레드 수를 늘리지는 않기 때문입니다.

Improving scalability with the asynchronous style (비동기 스타일로 확장성 향상)

하드웨어를 최대한 활용하고자 하는 일부 개발자는 스레드 공유 스타일을 선호하여 요청당 스레드 스타일을 포기했습니다.
하나의 스레드에서 처음부터 끝까지 요청을 처리하는 대신 요청 처리 코드는 스레드가 다른 요청을 처리할 수 있도록 I/O 작업이 완료될 때까지 대기할 때 해당 스레드를 풀로 반환합니다.
코드가 I/O를 기다릴 때가 아니라 계산을 수행할 때만 스레드를 유지하는 이 세분화된 스레드 공유는 많은 수의 스레드를 소비하지 않고도 많은 수의 동시 작업을 허용합니다.
이는 OS 스레드의 부족 으로 인한 처리량의 제한을 제거하지만 높은 비용이 듭니다.
I/O 작업이 완료될 때까지 기다리지 않고 나중에 완료 신호를 콜백에 보내는 별도의 I/O 메서드 집합을 사용하는 프로그래밍 스타일.
전용 스레드가 없으면 개발자는 요청 처리 논리를 일반적으로 람다 식으로 작성된 작은 단계로 분해한 다음 API를 사용하여 순차적 파이프라인으로 구성해야 합니다(예를 들어 CompletableFuture 또는 소위 “반응형” 프레임워크 참조 ) . ).
따라서 루프 및 블록과 같은 언어의 기본 순차 구성 연산자를 버립니다 try/catch.

비동기 스타일에서는 요청의 각 단계가 다른 스레드에서 실행될 수 있으며 모든 스레드는 인터리브 방식으로 다른 요청에 속하는 단계를 실행합니다.
이는 프로그램 동작을 이해하는 데 깊은 영향을 미칩니다. 스택 추적은 사용 가능한 컨텍스트를 제공하지 않으며 디버거는 요청 처리 논리를 단계별로 실행할 수 없으며 프로파일러는 작업 비용을 호출자와 연결할 수 없습니다.
람다 식 구성은 짧은 파이프라인에서 데이터를 처리하기 위해 Java의 스트림 API를 사용할 때 관리할 수 있지만 애플리케이션의 모든 요청 처리 코드를 이 방식으로 작성해야 하는 경우 문제가 됩니다.
이 프로그래밍 스타일은 애플리케이션의 동시성 단위(비동기 파이프라인)가 더 이상 플랫폼의 동시성 단위가 아니기 때문에 Java 플랫폼과 상충됩니다.

Preserving the thread-per-request style with virtual threads (가상 스레드로 요청당 스레드 스타일 유지)

애플리케이션이 플랫폼과 조화를 이루면서 확장할 수 있도록 하려면 스레드를 보다 효율적으로 구현하여
요청당 스레드 스타일을 유지하기 위해 노력해야 스레드가 더 많아질 수 있습니다.
운영 체제는 다른 언어와 런타임이 다른 방식으로 스레드 스택을 사용하기 때문에 OS 스레드를 보다 효율적으로 구현할 수 없습니다.
그러나 Java 런타임이 OS 스레드에 대한 일대일 대응을 분리하는 방식으로 Java 스레드를 구현하는 것은 가능합니다.
운영 체제가 큰 가상 주소 공간을 제한된 양의 물리적 RAM에 매핑하여 메모리가 풍부한 것처럼 보이게 하는 것처럼
Java 런타임은 많은 수의 가상 스레드를 적은 수의 OS 스레드에 매핑하여 스레드가 많은 것처럼 보이게 할 수 있습니다.

가상 스레드는 특정 java.lang.ThreadOS 스레드에 연결되지 않은 인스턴스입니다.
대조적으로 플랫폼 스레드는 java.lang.Thread OS 스레드 주변의 얇은 래퍼로 전통적인 방식으로 구현된 인스턴스입니다.

요청당 스레드 스타일의 애플리케이션 코드는 전체 요청 기간 동안 가상 스레드에서 실행될 수 있지만
가상 스레드는 CPU에서 계산을 수행하는 동안에만 OS 스레드를 소비합니다. 결과는 투명하게 달성된다는 점을 제외하면 비동기식 스타일과 동일한 확장성입니다.
가상 스레드에서 실행 중인 코드가 java.*API, 런타임은 비차단 OS 호출을 수행하고 나중에 다시 시작할 수 있을 때까지
가상 스레드를 자동으로 일시 중단합니다. Java 개발자에게 가상 스레드는 생성 비용이 저렴하고 거의 무한대로 풍부한 스레드일 뿐입니다.
하드웨어 활용도는 최적에 가까워 높은 수준의 동시성과 결과적으로 높은 처리량을 허용하는 동시에 응용 프로그램은
Java 플랫폼 및 해당 도구의 다중 스레드 설계와 조화를 이룹니다.

Implications of virtual threads (가상 스레드의 의미)

가상 스레드는 저렴하고 풍부하므로 풀링해서는 안 됩니다. 모든 애플리케이션 작업에 대해 새로운 가상 스레드를 생성해야 합니다.
따라서 대부분의 가상 스레드는 수명이 짧고 얕은 호출 스택을 가지며 단일 HTTP 클라이언트 호출 또는 단일 JDBC 쿼리만큼 적게 수행됩니다.
반대로 플랫폼 스레드는 무겁고 비용이 많이 들기 때문에 종종 풀링되어야 합니다.
수명이 길고 호출 스택이 깊으며 많은 작업 간에 공유되는 경향이 있습니다.

요약하면 가상 스레드는 하드웨어를 최적으로 활용하면서 Java 플랫폼의 설계와 조화를 이루는 안정적인 요청당 스레드 스타일을 유지합니다.
가상 스레드를 사용하는 데 새로운 개념을 학습할 필요는 없지만 오늘날의 높은 스레드 비용에 대처하기 위해 학습하지 않는 습관을 개발해야 할 수도 있습니다.
가상 스레드는 애플리케이션 개발자를 도울 뿐만 아니라 프레임워크 설계자가 확장성을 손상시키지 않으면서 플랫폼 설계와 호환되는 사용하기
쉬운 API를 제공하는 데도 도움이 됩니다.

Description

java.lang.Thread 오늘날 JDK의 모든 인스턴스는 플랫폼 스레드 입니다.
플랫폼 스레드는 기본 OS 스레드에서 Java 코드를 실행하고 코드의 전체 수명 동안 OS 스레드를 캡처합니다.
플랫폼 스레드 수는 OS 스레드 수로 제한됩니다.

가상 스레드는 java.lang.Thread 기본 OS 스레드에서 Java 코드를 실행하지만 코드의 전체 수명 동안 OS 스레드를 캡처하지 않는 인스턴스입니다.
이는 많은 가상 스레드가 동일한 OS 스레드에서 Java 코드를 실행하여 효과적으로 공유할 수 있음을 의미합니다.
플랫폼 스레드가 소중한 OS 스레드를 독점하는 반면 가상 스레드는 그렇지 않습니다.
가상 스레드의 수는 OS 스레드의 수보다 훨씬 클 수 있습니다.

가상 스레드는 OS가 아닌 JDK에서 제공하는 경량 스레드 구현입니다.
다른 다중 스레드 언어(예: Go의 고루틴 및 Erlang의 프로세스)에서 성공한 사용자 모드 스레드 의 한 형태입니다.
사용자 모드 스레드는 OS 스레드가 아직 성숙하고 널리 보급되지 않은 초기 Java 버전에서 소위 “그린 스레드” 로 기능하기도 했습니다.
그러나 Java의 그린 스레드는 모두 하나의 OS 스레드(M:1 스케줄링)를 공유했으며 결국 OS 스레드용 래퍼로 구현된 플랫폼 스레드(1:1 스케줄링)보다 성능이 뛰어났습니다.
가상 스레드는 M:N 스케줄링을 사용하는데, 여기서 많은 수(M)의 가상 스레드가 더 적은 수(N)의 OS 스레드에서 실행되도록 스케줄됩니다.

Using virtual threads vs. platform threads (가상 스레드 대 플랫폼 스레드 사용)

개발자는 가상 스레드 또는 플랫폼 스레드를 사용할지 여부를 선택할 수 있습니다.
다음은 다수의 가상 스레드를 생성하는 예제 프로그램입니다.
프로그램은 먼저 제출된 각 작업에 대해 새 가상 스레드를 생성할 ExecutorService를 얻습니다.
그런 다음 10,000개의 작업을 제출하고 모두 완료될 때까지 기다립니다.

1
2
3
4
5
6
7
8
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

이 예제의 작업은 간단한 코드(1초 동안 절전 모드)이며 최신 하드웨어는 이러한 코드를 동시에 실행하는 10,000개의 가상 스레드를 쉽게 지원할 수 있습니다.
배후에서 JDK는 소수의 OS 스레드(아마도 하나 정도)에서 코드를 실행합니다.

이 프로그램 이 ExecutorService. Executors.newCachedThreadPool()는 ExecutorService 10,000개의 플랫폼 스레드와 10,000개의 OS 스레드를 생성하려고 시도하며
시스템과 운영 체제에 따라 프로그램이 충돌할 수 있습니다.

대신 프로그램이 ExecutorService 풀에서 플랫폼 스레드를 가져오는 Executors.newFixedThreadPool(200).
그러면 ExecutorService10,000개의 모든 작업에서 공유할 200개의 플랫폼 스레드가 생성되므로 많은 작업이 동시가 아닌 순차적으로 실행되고 프로그램을 완료하는 데
오랜 시간이 걸립니다. 이 프로그램의 경우 200개의 플랫폼 스레드가 있는 풀은 초당 200개의 작업 처리량만 달성할 수 있는 반면
가상 스레드는 초당 약 10,000개의 작업 처리량을 달성할 수 있습니다(충분한 워밍업 후).
또한 10_000예제 프로그램의 가 로 변경되면 1_000_000프로그램은 1,000,000개의 작업을 제출하고 동시에 실행되는 1,000,000개의 가상 스레드를 생성하며
(충분한 워밍업 후) 초당 약 1,000,000개의 작업 처리량을 달성합니다.

이 프로그램의 작업이 단순히 휴면 상태가 아니라 1초 동안 계산(예: 거대한 배열 정렬)을 수행했다면
가상 스레드이든 플랫폼 스레드든 프로세서 코어 수 이상으로 스레드 수를 늘리는 것은 도움이 되지 않습니다.
가상 스레드는 더 빠른 스레드가 아닙니다. 가상 스레드는 플랫폼 스레드보다 더 빠르게 코드를 실행하지 않습니다.
속도(낮은 대기 시간)가 아니라 확장성(높은 처리량)을 제공하기 위해 존재합니다.
플랫폼 스레드보다 더 많을 수 있으므로 리틀의 법칙에 따라 더 높은 처리량에 필요한 더 높은 동시성을 가능하게 합니다.

달리 말하면 가상 스레드는 다음과 같은 경우 애플리케이션 처리량을 크게 향상시킬 수 있습니다.

  • 동시 작업의 수가 많고(수천 개 이상),
  • 이 경우 프로세서 코어보다 더 많은 스레드가 있으면 처리량을 향상시킬 수 없기 때문에 워크로드는 CPU에 제한되지 않습니다.

가상 스레드는 이러한 응용 프로그램이 대기 시간의 대부분을 소비하는 많은 수의 동시 작업으로 구성되기 때문에
일반적인 서버 응용 프로그램의 처리량을 향상시키는 데 도움이 됩니다.

가상 스레드는 플랫폼 스레드가 실행할 수 있는 모든 코드를 실행할 수 있습니다.
특히 가상 스레드는 플랫폼 스레드와 마찬가지로 스레드 로컬 변수 및 스레드 중단을 지원합니다.
이는 요청을 처리하는 기존 Java 코드가 가상 스레드에서 쉽게 실행됨을 의미합니다. 많은 서버 프레임워크는 들어오는
모든 요청에 대해 새로운 가상 스레드를 시작하고 그 안에서 애플리케이션의 비즈니스 로직을 실행하여 이 작업을 자동으로 수행하도록 선택할 것입니다.

다음은 다른 두 서비스의 결과를 집계하는 서버 애플리케이션의 예입니다.
handle가상의 서버 프레임워크(표시되지 않음)는 각 요청에 대해 새로운 가상 스레드를 생성하고 해당 가상 스레드에서 애플리케이션의 코드를 실행합니다.
그러면 애플리케이션 코드는 두 개의 새 가상 스레드를 생성하여 ExecutorService 첫 번째 예와 동일한 방법으로 리소스를 동시에 가져옵니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

oid 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);
}
}

간단한 차단 코드가 있는 이와 같은 서버 응용 프로그램은 많은 수의 가상 스레드를 사용할 수 있기 때문에 잘 확장됩니다.

Executor.newVirtualThreadPerTaskExecutor() 가상 스레드를 생성하는 유일한 방법은 아닙니다.
아래에서 설명하는 새 java.lang.Thread.BuilderAPI는 가상 스레드를 만들고 시작할 수 있습니다.
또한 구조화된 동시성은 특히 이 서버 예제와 유사한 코드에서 가상 스레드를 생성하고 관리하기 위한 보다 강력한 API를 제공합니다.
이에 따라 스레드 간의 관계가 플랫폼과 해당 도구에 알려집니다.

Virtual threads are a preview API, disabled by default (가상 스레드는 기본적으로 비활성화된 미리보기 API 입니다.)

위의 프로그램은 이 Executors.newVirtualThreadPerTaskExecutor() 방법을 사용하므로 JDK 19에서 실행하려면 다음과 같이 미리 보기 API를 활성화해야 합니다.

  • javac –release 19 –enable-preview Main.java 로 프로그램을 컴파일 하고 java –enable-preview 실행
  • 소스 코드 런처를 사용할 때 java –source 19 –enable-preview Main.java; 로 프로그램을 실행하십시오 . 또는,
  • jshell을 사용할 때 로 시작하십시오 jshell –enable-preview.

Do not pool virtual threads(가상 스레드를 풀링하지 않음)

ExecutorService 개발자는 일반적으로 응용 프로그램 코드를 기존 ExecutorService스레드 풀 기반 에서 작업당 가상 스레드로 마이그레이션합니다.
모든 리소스 풀과 마찬가지로 스레드 풀은 값비싼 리소스를 공유하기 위한 것이지만 가상 스레드는 비용이 많이 들지 않으며 풀링할 필요가 전혀 없습니다.

개발자는 때때로 스레드 풀을 사용하여 제한된 리소스에 대한 동시 액세스를 제한합니다.
예를 들어 서비스가 20개 이상의 동시 요청을 처리할 수 없는 경우 크기 20의 풀에 제출된 작업을 통해
서비스에 대한 모든 액세스를 수행하면 이를 보장할 수 있습니다.
플랫폼 스레드의 높은 비용으로 인해 스레드 풀이 유비쿼터스화되었기 때문에 이 관용구도 유비쿼터스가 되었지만
개발자는 동시성을 제한하기 위해 가상 스레드를 풀링하려는 유혹을 받아서는 안 됩니다.
제한된 리소스에 대한 액세스를 보호하기 위해 세마포어와 같이 해당 목적을 위해 특별히 설계된 구조를 사용해야 합니다.
이는 스레드 풀보다 더 효과적이고 편리하며 스레드 로컬 데이터가 실수로 한 작업에서 다른 작업으로 누출될 위험이 없기 때문에 더 안전합니다.

Observing virtual threads(가상 스레드 관찰)

명확한 코드를 작성하는 것이 전부는 아닙니다.
실행 중인 프로그램의 상태를 명확하게 표시하는 것도 문제 해결, 유지 관리 및 최적화에 필수적이며
JDK는 오랫동안 스레드를 디버그, 프로필 및 모니터링하는 메커니즘을 제공했습니다. 이러한 도구는 결국 java.lang.Thread.

Java 디버거는 가상 스레드를 단계별로 실행하고, 호출 스택을 표시하고, 스택 프레임의 변수를 검사할 수 있습니다.
JDK의 낮은 오버헤드 프로파일링 및 모니터링 메커니즘인 JFR(JDK Flight Recorder)은
애플리케이션 코드의 이벤트(예: 개체 할당 및 I/O 작업)를 올바른 가상 스레드와 연결할 수 있습니다.
이러한 도구는 비동기 스타일로 작성된 애플리케이션에 대해 이러한 작업을 수행할 수 없습니다.
이 스타일에서 작업은 스레드와 관련이 없으므로 디버거는 작업의 상태를 표시하거나 조작할 수 없으며
프로파일러는 작업이 I/O를 기다리는 데 소요되는 시간을 알 수 없습니다.

스레드 덤프는 요청당 스레드 스타일로 작성된 응용 프로그램 문제 해결을 위한 또 다른 인기 있는 도구입니다.
불행하게도 jstack 또는 jcmd 로 얻은 JDK의 기존 스레드 덤프는 단순한 스레드 목록을 제공합니다.
이는 수십 또는 수백 개의 플랫폼 스레드에 적합하지만 수천 또는 수백만 개의 가상 스레드에는 적합하지 않습니다.
따라서 가상 스레드를 포함하도록 기존의 스레드 덤프를 확장하지 않고 의미 있는 방식으로
모두 그룹화된 플랫폼 스레드와 함께 가상 스레드를 제공하기 위해 새로운 종류의 스레드 덤프를 도입할 것입니다.
프로그램이 구조화된 동시성을 사용할 때 스레드 간의 보다 풍부한 관계를 표시할 수 있습니다 .

많은 스레드를 시각화하고 분석하면 도구의 이점을 얻을 수 있으므로 는 일반 텍스트 외에도 JSON 형식으로 새 스레드 덤프를 내보낼 수 있습니다.

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

ExecutorService 새 스레드 덤프 형식은 네트워크 I/O 작업에서 차단된 가상 스레드와 위에 표시된
태스크당 새 스레드에 의해 생성된 가상 스레드를 나열합니다.
개체 주소, 잠금, JNI 통계, 힙 통계 및 기존 스레드 덤프에 표시되는 기타 정보는 포함되지 않습니다.
또한 많은 스레드를 나열해야 할 수 있으므로 새 스레드 덤프를 생성해도 응용 프로그램이 일시 중지되지 않습니다.

다음은 JSON 뷰어에서 렌더링된 위 의 두 번째 예와 유사한 애플리케이션에서 가져온 스레드 덤프의 예입니다 (확대하려면 클릭).

가상 스레드는 JDK에서 구현되고 특정 OS 스레드에 연결되지 않기 때문에 OS에서는 보이지 않으며 존재를 인식하지 못합니다.
OS 레벨 모니터링은 JDK 프로세스가 가상 스레드보다 적은 수의 OS 스레드를 사용함을 관찰합니다.

Scheduling virtual threads(가상 스레드 예약)

유용한 작업을 수행하려면 스레드를 예약해야 합니다. 즉, 프로세서 코어에서 실행하도록 할당해야 합니다.
OS 스레드로 구현되는 플랫폼 스레드의 경우 JDK는 OS의 스케줄러에 의존합니다.
반대로 가상 스레드의 경우 JDK에는 자체 스케줄러가 있습니다.
가상 스레드를 프로세서에 직접 할당하는 대신 JDK의 스케줄러는 가상 스레드를 플랫폼 스레드에 할당합니다(앞서 언급한 가상 스레드의 M:N 스케줄링).
그런 다음 플랫폼 스레드는 평소와 같이 OS에 의해 예약됩니다.

ForkJoinPoolJDK의 가상 스레드 스케줄러는 FIFO 모드로 작동하는 작업 도용입니다.
스케줄러의 병렬성 은 가상 스레드 스케줄링을 위해 사용 가능한 플랫폼 스레드의 수입니다.
기본적으로 사용 가능한 프로세서 수와 동일 하지만 시스템 속성으로 조정할 수 있습니다
jdk.virtualThreadScheduler.parallelism. 이것은 예를 들어 병렬 스트림 구현에 사용되고
LIFO 모드에서 작동하는 공통 풀과 구별ForkJoinPool 된다는 점에 유의하십시오.

스케줄러가 가상 스레드를 할당하는 플랫폼 스레드를 가상 스레드의 캐리어 라고 합니다.
가상 스레드는 수명 동안 다른 캐리어에서 예약될 수 있습니다. 즉, 스케줄러는 가상 스레드와 특정 플랫폼 스레드 간의 선호도를 유지하지 않습니다.
Java 코드의 관점에서 실행 중인 가상 스레드는 현재 캐리어와 논리적으로 독립적입니다.

  • 캐리어의 ID는 가상 스레드에서 사용할 수 없습니다. 에서 반환되는 값은 Thread.currentThread()항상 가상 스레드 자체입니다.
  • 캐리어와 가상 스레드의 스택 추적은 별개입니다. 가상 스레드에서 발생한 예외에는 캐리어의 스택 프레임이 포함되지 않습니다. 스레드 덤프는 캐리어의 스택 프레임을 가상 스레드의 스택에 표시하지 않으며 그 반대도 마찬가지입니다.
  • 캐리어의 스레드 로컬 변수는 가상 스레드에서 사용할 수 없으며 그 반대도 마찬가지입니다.

또한 Java 코드의 관점에서 볼 때 가상 스레드와 해당 캐리어가 일시적으로 OS 스레드를 공유한다는 사실은 보이지 않습니다.
반대로 네이티브 코드의 관점에서 볼 때 가상 스레드와 캐리어는 모두 동일한 네이티브 스레드에서 실행됩니다.
따라서 동일한 가상 스레드에서 여러 번 호출되는 네이티브 코드는 호출할 때마다 다른 OS 스레드 식별자를 관찰할 수 있습니다.

스케줄러는 현재 가상 스레드에 대한 시간 공유를 구현하지 않습니다. 시간 공유는 할당된 양의 CPU 시간을 사용한 스레드의 강제 선점입니다.
상대적으로 적은 수의 플랫폼 스레드가 있고 CPU 사용률이 100%일 때 시분할이 일부 작업의 대기 시간을 줄이는 데 효과적일 수 있지만
시분할이 백만 개의 가상 스레드에서만큼 효과적일지는 확실하지 않습니다.

Executing virtual threads(가상 스레드 실행)

가상 스레드를 활용하기 위해 프로그램을 다시 작성할 필요는 없습니다.
가상 스레드는 응용 프로그램 코드가 명시적으로 제어를 스케줄러로 되돌려줄 것을 요구하거나 기대하지 않습니다.
즉, 가상 스레드는 협력적이 지 않습니다. 사용자 코드는 플랫폼 스레드가 프로세서 코어에 할당되는 방법 또는 시기에 대해 가정하는 것 이상으로
가상 스레드가 플랫폼 스레드에 할당되는 방법 또는 시기에 대해 가정해서는 안 됩니다.

가상 스레드에서 코드를 실행하기 위해 JDK의 가상 스레드 스케줄러는 가상 스레드를 플랫폼 스레드에 마운트하여
플랫폼 스레드에서 실행할 가상 스레드를 할당합니다.
이렇게 하면 플랫폼 스레드가 가상 스레드의 캐리어가 됩니다.
나중에 일부 코드를 실행한 후 가상 스레드는 캐리어에서 마운트 해제 할 수 있습니다.
이 시점에서 플랫폼 스레드는 사용 가능하므로 스케줄러가 다른 가상 스레드를 플랫폼에 마운트하여 다시 캐리어로 만들 수 있습니다.

일반적으로 가상 스레드는 I/O 또는 JDK의 다른 차단 작업(예: BlockingQueue.take().
차단 작업이 완료될 준비가 되면(예: 소켓에 바이트가 수신됨) 스케줄러에 가상 스레드를 다시 제출하고 스케줄러는 가상 스레드를 캐리어에 탑재하여 실행을 재개합니다.

가상 스레드의 마운트 및 마운트 해제는 OS 스레드를 차단하지 않고 자주 투명하게 발생합니다.
예를 들어 이전에 표시된 서버 응용 프로그램에는 차단 작업에 대한 호출이 포함된 다음 코드 줄이 포함되어 있습니다.

1
response.send(future1.get() + future2.get());

이러한 작업으로 인해 가상 스레드는 일반적으로 각 호출에 대해 한 번, send(…) 에서 get() I/O를 수행하는 동안 여러 번 마운트 및 마운트 해제됩니다

JDK에서 대부분의 차단 작업은 가상 스레드를 마운트 해제하여 캐리어와 기본 OS 스레드가 새로운 작업을 수행하도록 해제합니다.
그러나 JDK의 일부 차단 작업은 가상 스레드를 마운트 해제하지 않으므로 캐리어와 기본 OS 스레드를 모두 차단합니다.
이는 OS 수준(예: 많은 파일 시스템 작업) 또는 JDK 수준(예: Object.wait())의 제한 사항 때문입니다.
이러한 차단 작업을 구현하면 스케줄러의 병렬 처리를 일시적으로 확장하여 OS 스레드 캡처를 보상합니다.
결과적으로 스케줄러의 플랫폼 스레드 수가 ForkJoinPool일시적으로 사용 가능한 프로세서 수를 초과할 수 있습니다.
스케줄러가 사용할 수 있는 최대 플랫폼 스레드 수는 시스템 속성으로 조정할 수 있습니다 jdk.virtualThreadScheduler.maxPoolSize.

캐리어에 고정되어 있기 때문에 차단 작업 중에 가상 스레드를 마운트 해제할 수 없는 두 가지 시나리오가 있습니다 .

  • 블록이나 메서드 내에서 코드를 실행할 때 synchronized또는
  • native 메서드나 외부 함수를 실행할 때.

고정은 응용 프로그램을 잘못 만들지는 않지만 확장성을 방해할 수 있습니다.
가상 스레드가 I/O와 같은 차단 작업을 수행하거나 BlockingQueue.take()고정된 상태에서 작업이 지속되는 동안 해당 캐리어와 기본 OS 스레드가 차단됩니다.
오랜 기간 동안 빈번한 고정은 캐리어 캡처로 인해 애플리케이션의 확장성을 손상시킬 수 있습니다.

스케줄러는 병렬 처리를 확장하여 고정을 보상하지 않습니다.
대신 자주 synchronized실행되는 블록이나 메서드를 수정하여 빈번 하고
오래 지속되는 고정을 피하고 java.util.concurrent.locks.ReentrantLock대신 사용할 잠재적으로 긴 I/O 작업을 보호하세요.
synchronized드물게 사용되거나(예: 시작 시에만 수행됨) 메모리 내 작업을 보호하는 블록 및 메서드를 교체할 필요가 없습니다.
항상 그렇듯이 잠금 정책을 간단하고 명확하게 유지하기 위해 노력하십시오.

synchronized새로운 진단 기능은 코드를 가상 스레드로 마이그레이션하고 java.util.concurrent 의 특정 용도를 잠금으로 대체해야 하는지 여부를 평가하는 데 도움이 됩니다.

  • 고정된 동안 스레드가 차단되면 JFR(JDK Flight Recorder) 이벤트가 발생합니다( JDK Flight Recorder 참조 ).
  • 고정된 상태에서 스레드가 차단되면 시스템 속성이 jdk.tracePinnedThreads스택 추적을 트리거합니다. 함께 실행하면 -Djdk.tracePinnedThreads=full고정된 상태에서 스레드가 차단될 때 전체 스택 추적이 인쇄되며 네이티브 프레임과 프레임이 모니터를 강조 표시합니다. 함께 실행하면 -Djdk.tracePinnedThreads=short문제가 있는 프레임으로만 출력이 제한됩니다.

향후 릴리스에서는 위의 첫 번째 제한 사항( 내부 고정)을 제거할 수 있습니다 synchronized.
네이티브 코드와의 적절한 상호 작용을 위해서는 두 번째 제한 사항이 필요합니다.

Memory use and interaction with garbage collection(메모리 사용 및 가비지 수집과의 상호 작용)

가상 스레드의 스택은 Java의 가비지 수집된 힙에 스택 청크 개체 로 저장됩니다.
애플리케이션이 실행됨에 따라 스택이 확장 및 축소되어 메모리 효율성이 향상되고 임의 깊이의 스택(JVM의 구성된 플랫폼 스레드 스택 크기까지)을 수용할 수 있습니다.
이러한 효율성은 많은 수의 가상 스레드를 가능하게 하고 따라서 서버 애플리케이션에서 요청당 스레드 스타일의 지속적인 실행 가능성을 가능하게 합니다.

위의 두 번째 예 에서 가상의 프레임워크가 새 가상 스레드를 만들고 메서드를 호출하여 각 요청을 처리한다는 점을 상기하십시오 handle.
handle깊은 호출 스택의 끝에서 호출하더라도 (인증, 트랜잭션 등 이후) handle자체적으로 단기 작업만 수행하는 여러 가상 스레드를 생성합니다.
따라서 깊은 호출 스택이 있는 각 가상 스레드에 대해 적은 메모리를 사용하는 얕은 호출 스택이 있는 여러 가상 스레드가 있습니다.

가상 스레드에 필요한 힙 공간 및 가비지 수집기 활동의 양은 일반적으로 비동기 코드와 비교하기 어렵습니다.
백만 개의 가상 스레드에는 최소한 백만 개의 개체가 필요하지만 플랫폼 스레드 풀을 공유하는 백만 개의 작업도 마찬가지입니다.
또한 요청을 처리하는 애플리케이션 코드는 일반적으로 I/O 작업 전반에 걸쳐 데이터를 유지합니다.
요청당 스레드 코드는 해당 데이터를 힙의 가상 스레드 스택에 저장되는 로컬 변수에 보관할 수 있는
반면, 비동기식 코드는 파이프라인의 한 단계에서 다음 단계로 전달되는 힙 개체에 동일한 데이터를 보관해야 합니다.
한편으로 가상 스레드에 필요한 스택 프레임 레이아웃은 압축 개체보다 더 낭비입니다.
반면에, 가상 스레드는 다양한 상황에서(낮은 수준의 GC 상호 작용에 따라) 스택을 변경하고 재사용할 수 있는
반면, 비동기 파이프라인은 항상 새 개체를 할당해야 하므로 가상 스레드는 더 적은 할당이 필요할 수 있습니다.
전반적으로 요청당 스레드 대 비동기 코드의 힙 소비 및 가비지 수집기 활동은 거의 유사해야 합니다.
시간이 지남에 따라 가상 스레드 스택의 내부 표현이 훨씬 더 간결해질 것으로 예상됩니다.

플랫폼 스레드 스택과 달리 가상 스레드 스택은 GC 루트가 아니므로 여기에 포함된 참조는 동시 힙 스캔을 수행하는 G1과 같은 가비지 수집기에 의한
top-the-world 일시 중지에서 순회되지 않습니다. BlockingQueue.take()이것은 또한 가상 스레드가 예를 들어 에서 차단되고 다른 스레드가
가상 스레드나 대기열에 대한 참조를 얻을 수 없는 경우 스레드가 가비지 수집될 수 있음을 의미합니다 . 중단되거나 차단 해제됩니다.
물론 가상 스레드는 실행 중이거나 차단되어 차단 해제될 수 있는 경우 가비지 수집되지 않습니다.

가상 스레드의 현재 제한 사항은 G1 GC가 거대한 스택 청크 개체를 지원하지 않는다는 것입니다.
가상 스레드의 스택이 영역 크기의 절반(512KB 정도로 작을 수 있음)에 도달하면 a가 StackOverflowError발생할 수 있습니다.

Detailed changes(세부 변경 사항)

  • java.lang.Thread
  • 스레드 로컬 변수
  • java.util.concurrent
  • 네트워킹
  • java.io
  • 자바 네이티브 인터페이스(JNI)
  • 디버깅(JVM TI, JDWP 및 JDI)
  • JDK 비행 기록기(JFR)
  • JMX(Java 관리 확장)
  • java.lang.ThreadGroup

java.lang.Thread

다음과 같이 API를 업데이트합니다 java.lang.Thread.

Thread.Builder, Thread.ofVirtual()및 은 Thread.ofPlatform()가상 및 플랫폼 스레드를 생성하는 새로운 API입니다. 예를 들어,
Thread thread = Thread.ofVirtual().name("duke").unstarted(runnable);
라는 새로운 시작되지 않은 가상 스레드를 생성합니다

Thread.startVirtualThread(Runnable)가상 스레드를 생성하고 시작하는 편리한 방법입니다.

A는 Thread.Builder스레드 또는 를 생성할 수 있으며 ThreadFactory, 그러면 동일한 속성을 가진 여러 스레드를 생성할 수 있습니다.

Thread.isVirtual()스레드가 가상 스레드인지 여부를 테스트합니다.

의 새로운 오버로드 Thread.join및 의 Thread.sleep인스턴스로 대기 및 절전 시간을 수락합니다 java.time.Duration.

새로운 최종 메서드는 Thread.threadId()스레드의 식별자를 반환합니다. 기존의 최종이 아닌 메서드는 Thread.getId()이제 더 이상 사용되지 않습니다.

Thread.getAllStackTraces()이제 모든 스레드가 아닌 모든 플랫폼 스레드의 맵을 반환합니다.

그렇지 않으면 API java.lang.Thread가 변경되지 않습니다.
클래스 에 의해 정의된 생성자는 Thread 이전과 같이 플랫폼 스레드를 생성합니다.
새로운 공개 생성자가 없습니다.

가상 스레드와 플랫폼 스레드 간의 주요 API 차이점은 다음과 같습니다.

공용 Thread생성자는 가상 스레드를 만들 수 없습니다.

가상 스레드는 항상 데몬 스레드입니다. 이 Thread.setDaemon(boolean)메서드는 가상 스레드를 데몬이 아닌 스레드로 변경할 수 없습니다.

가상 스레드는 의 고정 우선 순위를 갖습니다 Thread.NORM_PRIORITY. 이 Thread.setPriority(int)방법은 가상 스레드에 영향을 주지 않습니다.
이 제한 사항은 향후 릴리스에서 다시 검토될 수 있습니다.

가상 스레드는 스레드 그룹의 활성 구성원이 아닙니다.
가상 스레드에서 호출되면 Thread.getThreadGroup()이름이 있는 자리 표시자 스레드 그룹을 반환합니다 “VirtualThreads”.
API Thread.Builder는 가상 스레드의 스레드 그룹을 설정하는 방법을 정의하지 않습니다.

가상 스레드는 세트로 실행할 때 권한이 없습니다 SecurityManager.

stop()가상 스레드는 , suspend()또는 메서드를 지원하지 않습니다 resume(). 이러한 메서드는 가상 스레드에서 호출될 때 예외를 throw합니다.

Thread-local variables(스레드 로컬 변수)

가상 스레드는 플랫폼 스레드와 마찬가지로 스레드 로컬 변수(ThreadLocal) 및 상속 가능한 스레드 로컬 변수(InheritableThreadLocal)를 지원하므로
스레드 로컬을 사용하는 기존 코드를 실행할 수 있습니다.
그러나 가상 스레드가 매우 많을 수 있으므로 신중하게 고려한 후 스레드 로컬을 사용하십시오.
특히 스레드 풀에서 동일한 스레드를 공유하는 여러 작업 간에 비용이 많이 드는 리소스를 풀링하기 위해 스레드 로컬을 사용하지 마십시오.
가상 스레드는 풀링되지 않아야 합니다. 각 스레드는 수명 기간 동안 단일 작업만 실행하도록 되어 있기 때문입니다.
java.base수백만 개의 스레드로 실행할 때 메모리 공간을 줄이기 위해 가상 스레드를 준비하기 위해 모듈 에서 스레드 로컬의 많은 사용을 제거했습니다 .

  • API 는 스레드를 생성할 때 스레드 로컬을 옵트아웃하는 방법을Thread.Builder 정의합니다.
  • 또한 상속 가능한 thread-locals 의 초기 값 상속을 거부하는 방법을 정의합니다.
    스레드 로컬을 지원하지 않는 스레드에서 호출되면 초기 값을 반환하고 예외를 throw합니다.ThreadLocal.get()ThreadLocal.set(T)
  • 레거시 컨텍스트 클래스 로더는 이제 상속 가능한 스레드 로컬처럼 작동하도록 지정되었습니다.
    Thread.setContextClassLoader(ClassLoader)스레드 로컬을 지원하지 않는 스레드에서 호출되면 예외가 발생합니다 .

범위 로컬 변수는 일부 사용 사례에서 스레드 로컬에 대한 더 나은 대안이 될 수 있습니다.

java.util.concurrent

잠금을 지원하는 원시 API는 java.util.concurrent.LockSupport이제 가상 스레드를 지원합니다.
가상 스레드를 파킹하면 다른 작업을 수행하기 위해 기본 플랫폼 스레드가 해제되고 가상 스레드를 파킹 해제하면 계속되도록 예약됩니다.
이 변경으로 LockSupport 이를 사용하는 모든 API(Locks, Semaphores, 차단 대기열 등)가 가상 스레드에서 호출될 때 정상적으로 파킹할 수 있습니다.

  • Executors.newThreadPerTaskExecutor(ThreadFactory)각 작업에 대해 새 스레드를 생성하는 Executors.newVirtualThreadPerTaskExecutor()을 생성합니다.
    ExecutorService이러한 메서드를 사용하면 스레드 풀 및 ExecutorService.

  • ExecutorService이제 extends AutoCloseable이므로 이 API를 위의 예와 같이 try-with-resource 구성과 함께 사용할 수 있습니다.

  • Future이제 완료된 작업의 결과 또는 예외를 가져오고 작업의 상태를 가져오는 메서드를 정의합니다.
    이러한 추가 기능을 결합하면 개체를 스트림의 요소로 쉽게 사용할 수 있고 Future, 퓨처 스트림을 필터링하여 완료된 작업을 찾은 다음
    이를 매핑하여 결과 스트림을 얻을 수 있습니다. 이러한 방법은 구조화된 동시성을 위해 제안된 API 추가에도 유용합니다.

Networking

java.net및 패키지 의 네트워킹 API 구현은 java.nio.channels이제 가상 스레드와 함께 작동합니다.
예를 들어 네트워크 연결 설정 또는 소켓에서 읽기를 차단하는 가상 스레드에 대한 작업은 다른 작업을 수행하기 위해 기본 플랫폼 스레드를 해제합니다.

중단 및 취소를 허용하기 위해 , java.net.Socket및 ServerSocket에 의해 정의된 차단 I/O 메서드는 이제 가상 스레드에서 호출될 때
중단 가능DatagramSocket 하도록 지정됩니다. 소켓에서 차단된 가상 스레드를 중단하면 스레드가 언파킹되고 소켓이 닫힙니다.
에서 가져올 때 이러한 유형의 소켓에 대한 I/O 작업 차단은 항상 인터럽트할 수 있으므로
이 변경 사항은 채널에서 가져올 때 해당 동작과 생성자로 생성될 때 이러한 API의 동작을 정렬합니다.InterruptibleChannel

java.io

이 java.io패키지는 바이트 및 문자 스트림에 대한 API를 제공합니다.
이러한 API의 구현은 심하게 동기화되며 가상 스레드에서 사용될 때 고정을 방지하기 위해 변경이 필요합니다.

close()배경으로 바이트 지향 입력/출력 스트림은 스레드로부터 안전하도록 지정되지 않았으며
스레드가 읽기 또는 쓰기 메서드에서 차단된 동안 호출될 때 예상되는 동작을 지정하지 않습니다.
대부분의 시나리오에서 여러 동시 스레드의 특정 입력 또는 출력 스트림을 사용하는 것은 의미가 없습니다.
문자 지향 리더/라이터도 스레드로부터 안전하도록 지정되지 않았지만 하위 클래스에 대한 잠금 개체를 노출합니다.
고정 외에도 이러한 클래스의 동기화는 문제가 있고 일관성이 없습니다.
예를 들어 잠금 개체가 아닌 스트림 개체에서 사용 InputStreamReader하고 동기화하는 스트림 디코더 및 인코더입니다 .OutputStreamWriter

고정을 방지하기 위해 구현은 이제 다음과 같이 작동합니다.

  • BufferedInputStream, BufferedOutputStream, BufferedReader, BufferedWriterPrintStreamPrintWriter,
    그리고 이제 직접 사용할 때 모니터가 아닌 명시적 잠금을 사용합니다 . 이러한 클래스는 하위 클래스로 분류될 때 이전과 같이 동기화됩니다.

  • InputStreamReader 에서 사용하는 스트림 디코더 및 인코더는 이제 OutputStreamWriter둘러싸는 InputStreamReader
    또는 OutputStreamWriter 와 동일한 잠금을 사용합니다

더 나아가 불필요한 잠금을 모두 제거하는 것은 이 JEP의 범위를 벗어납니다.

BufferedOutputStream또한 , BufferedWriter및 스트림 인코더 에서 사용하는 버퍼의 초기 크기는 OutputStreamWriter이제
힙에 많은 스트림 또는 작성자가 있을 때 메모리 사용량을 줄이기 위해 더 작아졌습니다. 소켓 연결에서 버퍼링된 스트림.

Java Native Interface (JNI)

IsVirtualThreadJNI는 개체가 가상 스레드인지 테스트하기 위해 하나의 새 함수를 정의합니다 .

그렇지 않으면 JNI 사양이 변경되지 않습니다.

Debugging

디버깅 아키텍처는 JVM TI(JVM Tool Interface), JDWP(Java Debug Wire Protocol) 및 JDI(Java Debug Interface)의 세 가지 인터페이스로 구성됩니다.
이제 세 가지 인터페이스 모두 가상 스레드를 지원합니다.

JVM TI 에 대한 업데이트는 다음과 같습니다.

  • jthread(즉, 객체에 대한 JNI 참조 ) 로 호출되는 대부분의 함수는 Thread가상 스레드에 대한 참조로 호출될 수 있습니다.
    소수의 함수, 즉 PopFrame, ForceEarlyReturn, StopThread, AgentStartFunction및 은 GetThreadCpuTime가상 스레드에서 지원되지 않습니다.
    기능 SetLocal*은 중단점 또는 단일 단계 이벤트에서 일시 중단된 가상 스레드의 최상위 프레임에서 로컬 변수를 설정하는 것으로 제한됩니다.

  • 이제 모든 스레드가 아닌 모든 플랫폼 스레드를 반환하도록 GetAllThreads및 함수 가 지정되었습니다.GetAllStackTraces

  • 초기 VM 시작 또는 힙 반복 중에 게시된 이벤트를 제외한 모든 이벤트는 가상 스레드 컨텍스트에서 호출된 이벤트 콜백을 가질 수 있습니다.

  • 일시 중단/재개 구현을 사용하면 디버거에서 가상 스레드를 일시 중단 및 재개할 수 있으며 가상 스레드가 마운트될 때 플랫폼 스레드를 일시 중단할 수 있습니다.

  • 새 기능인 은 can_support_virtual_threads에이전트가 가상 스레드의 스레드 시작 및 종료 이벤트를 보다 세밀하게 제어할 수 있도록 합니다.

  • 새로운 기능은 가상 스레드의 대량 일시 중지 및 재개를 지원합니다. 여기에는 can_support_virtual_threads 기능이 필요합니다.

기존 JVM TI 에이전트는 대부분 이전과 동일하게 작동하지만 가상 스레드에서 지원되지 않는 기능을 호출하는 경우 오류가 발생할 수 있습니다.
가상 스레드를 인식하지 못하는 에이전트가 가상 스레드를 사용하는 애플리케이션과 함께 사용되는 경우 이러한 오류가 발생합니다.
GetAllThreads플랫폼 스레드만 포함하는 배열을 반환하도록 변경하면 일부 에이전트에서 문제가 될 수 있습니다.
ThreadStart및 이벤트를 활성화하는 기존 에이전트는 ThreadEnd이러한 이벤트를 플랫폼 스레드로 제한하는 기능이 없기 때문에 성능 문제가 발생할 수 있습니다.

JDWP 에 대한 업데이트는 다음과 같습니다.

  • 새 명령을 사용하면 디버거가 스레드가 가상 스레드인지 테스트할 수 있습니다.
  • 명령 의 새 수정자는 EventRequest디버거가 스레드 시작 및 종료 이벤트를 플랫폼 스레드로 제한할 수 있도록 합니다.

JDI 에 대한 업데이트는 다음과 같습니다.

  • com.sun.jdi.ThreadReference스레드가 가상 스레드인지 여부를 테스트하는 새로운 방법입니다
  • 새로운 메서드는 플랫폼 스레드에 대한 요청에 대해 생성된 이벤트를 제한합니다 com.sun.jdi.request.ThreadStartRequest.com.sun.jdi.request.ThreadDeathRequest

위에서 언급한 것처럼 가상 스레드는 스레드 그룹에서 활성 스레드로 간주되지 않습니다.
따라서 JVM TI 함수 GetThreadGroupChildren, JDWP 명령 ThreadGroupReference/Children및 JDI 메서드 에서 반환된 스레드 목록
com.sun.jdi.ThreadGroupReference.threads()에는 플랫폼 스레드만 포함됩니다.

JDK Flight Recorder (JFR)

JFR은 몇 가지 새로운 이벤트로 가상 스레드를 지원합니다.

  • jdk.VirtualThreadStart가상 스레드 시작 및 종료를 나타 냅니다 jdk.VirtualThreadEnd. 이러한 이벤트는 기본적으로 비활성화되어 있습니다.
  • jdk.VirtualThreadPinned고정된 동안 즉, 플랫폼 스레드를 해제하지 않고 가상 스레드가 정지되었음을 나타냅니다( 토론 참조 ).
    이 이벤트는 기본적으로 활성화되며 임계값은 20ms입니다.
  • jdk.VirtualThreadSubmitFailed리소스 문제로 인해 가상 스레드 시작 또는 언파킹이 실패했음을 나타냅니다. 이 이벤트는 기본적으로 활성화되어 있습니다.

Java Management Extensions (JMX)

java.lang.management.ThreadMXBean플랫폼 스레드의 모니터링 및 관리만 지원합니다.
이 findDeadlockedThreads()메서드는 교착 상태에 있는 플랫폼 스레드의 주기를 찾습니다.
교착 상태에 있는 가상 스레드의 주기는 찾지 않습니다.

의 새로운 메서드는 위에서com.sun.management.HotSpotDiagnosticsMXBean 설명한 새로운 스타일의 스레드 덤프를 생성합니다.
이 메서드는 로컬 또는 원격 JMX 도구에서 플랫폼을 통해 간접적으로 호출할 수도 있습니다 .MBeanServer

java.lang.ThreadGroup

java.lang.ThreadGroup최신 애플리케이션에서 거의 사용되지 않고 가상 스레드 그룹화에 적합하지 않은 스레드 그룹화를 위한 레거시 API입니다.
우리는 지금 그것을 더 이상 사용하지 않고 성능을 저하시키며 구조화된 동시성 의 일부로 미래에 새로운 스레드 구성 구조를 도입할 것으로 예상합니다 .

배경으로 ThreadGroupAPI는 Java 1.0부터 시작됩니다. 원래 그룹의 모든 스레드 중지와 같은 작업 제어 작업을 제공하기 위한 것이었습니다.
java.util.concurrent최신 코드는 Java 5에 도입된 패키지 의 스레드 풀 API를 사용할 가능성이 더 높습니다 ThreadGroup.
초기 Java 릴리스에서 애플릿의 격리를 지원했지만 Java 보안 아키텍처는 Java 1.2에서 크게 발전했으며 스레드 그룹은 더 이상 중요한 역할을 하지 않았습니다.
ThreadGroup진단 목적에도 유용하도록 의도되었지만 그 역할은 API를 포함하여 Java 5에 도입된 모니터링 및 관리 기능으로 대체되었습니다 java.lang.management.

현재 거의 관련이 없는 것 외에도 ThreadGroupAPI 및 구현에는 여러 가지 중요한 문제가 있습니다.

  • 스레드 그룹을 파괴하는 API 및 메커니즘에 결함이 있습니다.
  • API는 구현이 그룹의 모든 라이브 스레드에 대한 참조를 갖도록 요구합니다. 이는 스레드 생성, 스레드 시작 및 스레드 종료에 동기화 및 경합 오버헤드를 추가합니다.
  • API는 enumerate()본질적으로 정확성이 있는 메서드를 정의합니다.
  • API는 본질적으로 교착 상태가 발생 suspend()하기 쉽고 안전하지 않은 , resume()및 stop()메서드를 정의합니다.

ThreadGroup이제 다음과 같이 지정, 더 이상 사용되지 않으며 성능이 저하됩니다.

  • 명시적으로 스레드 그룹을 제거하는 기능이 제거되었습니다. 최종적으로 더 이상 사용되지 않는 destroy()메서드는 아무 작업도 수행하지 않습니다.
  • 데몬 스레드 그룹의 개념이 제거되었습니다. 최종적으로 더 이상 사용되지 않는 메서드에 의해 설정되고 검색된 데몬 상태가 setDaemon(boolean)무시 isDaemon()됩니다.
  • 구현 시 더 이상 하위 그룹에 대한 강력한 참조를 유지하지 않습니다. 스레드 그룹은 이제 그룹에 활성 스레드가 없고 스레드 그룹을 활성 상태로 유지하는 다른 항목이 없을 때 가비지 수집 대상이 됩니다.
  • 최종적으로 사용되지 않는 suspend(), resume()및 stop()메소드는 항상 예외를 발생시킵니다.

Alternatives

  • 계속해서 비동기 API를 사용합니다. 비동기식 API는 동기식 API와 통합하기 어렵고, 동일한 I/O 작업의 두 가지 표현으로 분리된 세계를 생성하며,
    문제 해결, 모니터링, 디버깅을 위한 컨텍스트로 플랫폼에서 사용할 수 있는 일련의 작업에 대한 통합 개념을 제공하지 않습니다. , 그리고 프로파일링 목적.

  • 구문 스택리스 코루틴 (예: async/await )을 Java 언어에 추가합니다 .
    이들은 사용자 모드 스레드보다 구현하기 쉽고 일련의 작업 컨텍스트를 나타내는 통합 구조를 제공합니다.
    그러나 그 구성은 새롭고 여러 측면에서 유사하지만 일부 미묘한 방식에서 다른 스레드와 별개입니다.
    그것은 스레드용으로 설계된 API와 코루틴용으로 설계된 API 사이에서 세계를 분할하고 플랫폼 및 해당 도구의 모든 계층에 도입되는
    새로운 스레드와 유사한 구성을 요구합니다.
    이것은 생태계가 채택하는 데 더 오래 걸리며 사용자 모드 스레드만큼 플랫폼과 우아하고 조화롭지 않을 것입니다.

구문적 코루틴을 채택한 대부분의 언어는 사용자 모드 스레드(예: Kotlin),
레거시 의미 체계 보장(예: 본질적으로 단일 스레드 JavaScript) 또는 언어별 기술적 제약(예: C++)을 구현할 수 없기 때문에 그렇게 했습니다. ).
이러한 제한은 Java에 적용되지 않습니다.

  • java.lang.Thread_ Thread25년 동안 학급이 쌓아온 불필요한 짐을 버릴 수 있는 기회가 될 것이다.
    우리는 이 접근 방식의 여러 변형을 탐색하고 프로토타입을 만들었지만 모든 경우에 기존 코드를 실행하는 방법에 대한 문제와 씨름했습니다.

주된 문제는 이것이 Thread.currentThread()직간접적으로 기존 코드에 광범위하게 사용된다는 것입니다(예: 잠금 소유권 결정 또는 스레드 로컬 변수).
이 메서드는 현재 실행 스레드를 나타내는 개체를 반환해야 합니다. 사용자 모드 스레드를 나타내는 새 클래스를 도입한 경우 처럼 보이지만
사용자 모드 스레드 개체에 위임하는 currentThread()일종의 래퍼 개체를 반환해야 합니다 .Thread

두 개체가 현재 실행 스레드를 나타내는 것은 혼란스러울 수 있으므로 결국 이전 ThreadAPI를 유지하는 것이 큰 장애물이 아니라는 결론을 내렸습니다.
와 같은 몇 가지 방법을 제외하고 개발자는 API를 직접 currentThread()사용하는 경우가 거의 없습니다 .
Thread그들은 주로 ExecutorService. 시간이 지남에 따라 더 이상 사용되지 않는 메서드 를 사용하지 않고 제거하여 Thread클래스 및
관련 클래스 에서 불필요한 짐을 버릴 것입니다 .ThreadGroup

Testing

  • 기존 테스트는 여기에서 제안하는 변경 사항이 실행되는 다양한 구성 및 실행 모드에서 예기치 않은 회귀를 일으키지 않도록 합니다.
  • jtreg가상 스레드 컨텍스트에서 기존 테스트를 실행할 수 있도록 테스트 도구를 확장할 것입니다 . 이렇게 하면 많은 테스트의 두 가지 버전이 필요하지 않습니다.
  • 새로운 테스트는 모든 신규 및 수정된 API와 가상 스레드를 지원하도록 변경된 모든 영역을 실행합니다.
  • 새로운 스트레스 테스트는 안정성과 성능에 중요한 영역을 대상으로 합니다.
  • 새로운 마이크로벤치마크는 성능이 중요한 영역을 대상으로 합니다.
  • 대규모 테스트를 위해 Helidon 및 Jetty를 포함한 여러 기존 서버를 사용할 것입니다.

Risks and Assumptions

이 제안의 주요 위험은 기존 API 및 해당 구현의 변경으로 인한 호환성 위험입니다.

java.io.BufferedInputStream, BufferedOutputStream, BufferedReader, BufferedWriter, PrintStream및 클래스 에서 사용되는 내부(및 문서화되지 않은)
잠금 프로토콜에 대한 개정은 PrintWriterI/O 메서드가 호출되는 스트림에서 동기화된다고 가정하는 코드에 영향을 줄 수 있습니다.
이러한 변경 사항은 이러한 클래스를 확장하고 슈퍼클래스에 의한 잠금을 가정하는 코드에 영향을 주지 않으며
해당 API에서 노출하는 잠금 개체를 확장 java.io.Reader하거나 사용하는 코드에도 영향을 주지 않습니다 .java.io.Writer

java.lang.ThreadGroup더 이상 스레드 그룹이 파괴 되는 것을 허용하지 않으며 더 이상 데몬
스레드 그룹 의 개념을 지원하지 않으며 해당 suspend(), resume()및 stop()메소드는 항상 예외를 발생시킵니다.

다음은 확장하는 코드에 영향을 미칠 수 있는 몇 가지 소스 비호환 API 변경 사항과 하나의 바이너리 비호환 변경 사항입니다 java.lang.Thread.

  • 기존 소스 파일의 코드가 확장되고 Thread하위 클래스의 메서드가 새 Thread메서드와 충돌하는 경우 파일은 변경 없이 컴파일되지 않습니다.
  • Thread.Builder중첩 인터페이스로 추가됩니다. 기존 소스 파일의 코드가 를 확장하고 라는
    Thread클래스를 가져오고 Builder하위 클래스의 코드가 “빌더”를 단순 이름으로 참조하는 경우 파일은 변경 없이 컴파일되지 않습니다.
  • Thread.threadId()스레드의 식별자를 반환하는 최종 메서드로 추가됩니다. 기존 소스 파일의 코드가 확장되고
    Thread하위 클래스가 매개 변수 없이 명명된 메서드를 선언하면 threadId컴파일되지 않습니다.
    확장하는 기존 컴파일된 코드가 있고 하위 클래스가 반환 유형이 있고 매개 변수가 없는 Thread이름의 메서드를 정의하는 경우
    하위 클래스가 로드되면 런타임에 throw됩니다.threadIdlongIncompatibleClassChangeError

가상 스레드 또는 새 API를 활용하는 최신 코드와 기존 코드를 혼합할 때 플랫폼 스레드와 가상 스레드 간의 몇 가지 동작 차이가 관찰될 수 있습니다.

  • 이 Thread.setPriority(int)방법은 항상 우선 순위가 있는 가상 스레드에 영향을 주지 않습니다 Thread.NORM_PRIORITY.

  • 이 Thread.setDaemon(boolean)메서드는 항상 데몬 스레드인 가상 스레드에 영향을 주지 않습니다.

  • 스레드 . stop(), suspend()및 메서드는 가상 스레드에서 호출될 때 resume()발생합니다 .UnsupportedOperationException

  • API Thread는 스레드 로컬 변수를 지원하지 않는 스레드 생성을 지원합니다.
    스레드 로컬을 지원하지 않는 스레드 컨텍스트에서 호출될 때 ThreadLocal.set(T)throw Thread.setContextClassLoader(ClassLoader)합니다
    .UnsupportedOperationException

  • Thread.getAllStackTraces()이제 모든 스레드의 맵이 아닌 모든 플랫폼 스레드의 맵을 반환합니다.

  • java.net.Socket, ServerSocket및 에 의해 정의된 차단 I/O 메서드는 DatagramSocket이제 가상 스레드 컨텍스트에서 호출될 때 인터럽트할 수 있습니다.
    기존 코드는 소켓 작업에서 차단된 스레드가 중단되어 스레드를 깨우고 소켓을 닫을 때 중단될 수 있습니다.

  • 가상 스레드는 의 활성 구성원이 아닙니다 ThreadGroup. Thread.getThreadGroup()가상 스레드에서 호출하면 “VirtualThreads” 비어 있는 더미 그룹이 반환됩니다.

  • 가상 스레드는 세트로 실행할 때 권한이 없습니다 SecurityManager.

  • JVM TI에서 GetAllThreads및 GetAllStackTraces함수는 가상 스레드를 반환하지 않습니다.
    ThreadStart및 이벤트를 활성화하는 기존 에이전트는 ThreadEnd 이벤트를 플랫폼 스레드로 제한하는 기능이 없기 때문에 성능 문제가 발생할 수 있습니다.

  • API java.lang.management.ThreadMXBean는 플랫폼 스레드의 모니터링 및 관리를 지원하지만 가상 스레드는 지원하지 않습니다.

  • 플래그 -XX:+PreserveFramePointer는 가상 스레드 성능에 크게 부정적인 영향을 미칩니다.

Dependencies

  • JDK 18의 JEP 416(Reimplement Core Reflection with Method Handles)은 VM 네이티브 리플렉션 구현을 제거했습니다.
    이렇게 하면 메서드가 반사적으로 호출될 때 가상 스레드가 정상적으로 주차할 수 있습니다.

  • JDK 13의 JEP 353(레거시 소켓 API 재구현) 및 JDK 15의 JEP 373(레거시 DatagramSocket API 재구현)은java.net.Socket , ServerSocket및 의
    구현을 DatagramSocket가상 스레드와 함께 사용하도록 설계된 새 구현으로 대체했습니다.

  • JDK 18의 JEP 418(Internet-Address Resolution SPI)은 호스트 이름 및 주소 조회를 위한 서비스 공급자 인터페이스를 정의했습니다.
    이렇게 하면 타사 라이브러리가 java.net.InetAddress호스트 조회 중에 스레드를 고정하지 않는 대체 해결 프로그램을 구현할 수 있습니다.

참조