이 자료는 javaSE jdk22 의 자료를 해석하고 테스트한 내용입니다.
원본 : https://docs.oracle.com/en/java/javase/22/core/virtual-threads.html#GUID-2BCFC2DD-7D84-4B0C-9222-97F9C7C6C521 (2024.04발행)
JAVA SE 22 API 레퍼런스 : https://docs.oracle.com/en/java/javase/22/docs/api/java.base/java/lang/Thread.Builder.OfVirtual.html
가상스레드를 생성하는 방법은 2가지가 있다.
(1) Thread 클래스와 Thread.Builder 인터페이스를 사용하는 방법
(2) Executor.newVirtualThreadPerTaskExecutor 메소드를 사용하는 법
아직 공식적으로 지원은 하지 않고 준비중이라고 합니다 ㅠㅠ
그냥 레퍼런스 해석만 하는 걸로 하겠습니다.
Thread 클래스와 Thread.Builder 인터페이스를 사용하는 방법
Thread thread = Thread.ofVirtual().start(() -> System.out.println("Hello"));
가상 스레드를 만들기 위해 Thread.Builder의 인스턴스를 생성하는 메서드 Thread.ofVirtual()을 생성한다.
Thread.Builder 인터페이스는 스레드 이름(name) 과 같은 일반 Thread의 프로퍼티와 함께 구현이 가능하다.
Thread.Builder builder = Thread.ofVirtual().name("MyThread"); //기존 스레드처럼 이름속성 사용 가능
Runnable task = () -> {
System.out.println("Running thread");
};
Thread t = builder.start(task); //신기하게 start에 인자로 Runnable 을 준다.
//기존 Thread는 생성자로 Runnable 객체를 부여했는데
System.out.println("Thread t name: " + t.getName());
Thread.Builder builder = Thread.ofVirtual().name("worker-", 0);
Runnable task = () -> {
System.out.println("Thread ID: " + Thread.currentThread().threadId());
};
Thread t1 = builder.start(task); //위에서 Thread ID 랜덤값 출력
t1.join();
System.out.println(t1.getName() + " terminated"); //worker-0 terminated
Thread t2 = builder.start(task); //위에서 Thread ID 랜덤값 출력
t2.join();
System.out.println(t2.getName() + " terminated"); //worker-1 terminated
참고로 name의 인자는 prefix 와 start이다.
즉 생성되는 가상 스레드의 접두사와 시작할 숫자를 의미하며 여기서는 worker-0 , worker-1 .. 같이 생성된다.

Executor.newVirtualThreadPerTaskExecutor 메소드를 사용하는 법
Executor는 스레드 관리와 생성을 어플리케이션에서 분리해서 관리할 수 있다.
(예전에 테스트를 할 때 fixedThreadPool 처럼 스레드 개수를 한정하는 등, 스레드 관련 처리를 전체적인 앱에서 분리해서 쓸 수 있다는 의미같다)
try (ExecutorService myExecutor = Executors.newVirtualThreadPerTaskExecutor()) {
Future<?> future = myExecutor.submit(() -> System.out.println("Running thread"));
future.get();
System.out.println("Task completed");
// ...
코드 설명
(1) Executors.newVirtualThreadPerTaskExecutor() 메서드로 ExecutorService를 생성했다.
(2) ExecutorService의 submit 메서드는 입력받은 Runnable을 실행시킨다. 리턴은 Future객체
(3) Future객체는 get() 을 이용해서 가상스레드가 완료되기를 기다린다. 왜냐면 get()을 통해 가상스레드에서 리턴하려는 값을 받을 수 있기 때문이다. 👉예시코드-사용자정보가져오기
(4) 위에서는 따로 get() 한 데이터를 처리하지 않고 가상메서드 완료를 기다리는 용도이므로
마지막에 Task Completed가 실행되고 종료된다. ( future -> Main 쓰레드 순서로 종료)
이 예시는 EchoServer , EchoClient 두개의 클래스로 구성이 되어있다.
EchoServer는 각 연결마다 포트 생성 및 새로운 가상스레드를 생성한다.
EchoClient는 위 서버에 연결해서 메세지를 보내는 프로그램이다.
EchoClient는 EchoServer와 소켓을 이용해 커넥션을 획득하고,
C->S 에게 메세지를 보내면 S->C에게 다시 메세지를 반환해주는 간단한 구조이다.
서버는 각 클라이언트 요청마다 가상의 스레드를 통해 동시에 서비스를 할 수 있다
public class EchoServer {
public static void main(String[] args) throws IOException {
//전달된 인수 개수가 1이 아니면 사용법을 리턴하고 종료 (포트 입력하세요 등)
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(); //클라이언트 요청 대기
//가상 스레드 시작!
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);
}
}
}
뭔가 코드가 길어보이지만 결론을 말하면
Server가 다수의 Client에게 서비스하기 위해 가상의 쓰레드를 활용할 수 있고,
특히 I/O 작업처럼 대기가 많이 발생하는 환경에서 가상의 쓰레드는 보다 효율적으로
자원을 사용하며 많은 Client에게 서비스를 할 수 있게 된다.