-
Java Virtual Thread 이론JAVA 2024. 4. 24. 00:56
Virtual Thread
Virtual Thread는 가벼운(lightweight) Thread로,
고성능 동시성 애플리케이션을 작성, 유지 및 디버깅하는 데 드는 노력을 줄여줍니다.
Virtual Thread에 대한 배경 정보는 JEP 444를 참조하세요.
Thread는 스케줄링될 수 있는 가장 작은 처리 단위입니다. 이는 다른 단위들과 동시에 — 그리고 대부분 독립적으로 — 실행됩니다. 이것은 java.lang.Thread의 인스턴스입니다. 스레드에는 두 가지 종류가 있는데, 플랫폼 스레드와 가상 스레드가 그것입니다.여기에서 "가상 스레드"는 자바의 새로운 기능 중 하나로, 기존의 플랫폼 스레드(heavyweight thread)에 비해 훨씬 가벼운 스레드를 말합니다. 이를 통해 동시성이 높은 애플리케이션을 더 쉽게 개발할 수 있게 됩니다. 가상 스레드는 자바의 java.lang.Thread 클래스의 인스턴스로 관리되며, 성능을 크게 향상시킬 수 있습니다.
플랫폼 스레드(Platform Thread)란?
플랫폼 스레드는 운영 체제(OS) 스레드를 둘러싼 얇은 래퍼로 구현됩니다. 플랫폼 스레드는 자바 코드를 기반 OS 스레드에서 실행하며, 해당 스레드의 전체 수명 동안 OS 스레드를 점유합니다. 따라서, 사용 가능한 플랫폼 스레드의 수는 OS 스레드의 수에 제한됩니다.
플랫폼 스레드는 일반적으로 큰 스레드 스택과 운영 체제가 관리하는 기타 자원을 가지고 있으며, 모든 유형의 작업을 실행하는 데 적합하지만 제한된 자원일 수 있습니다.
가상 스레드(Virtual Thread)란?
가상 스레드 역시 java.lang.Thread의 인스턴스로, 플랫폼 스레드와 유사합니다. 그러나 가상 스레드는 특정 OS 스레드에 연결되어 있지 않습니다. 가상 스레드는 OS 스레드에서 코드를 실행하지만, 가상 스레드에서 실행 중인 코드가 블로킹 I/O 작업을 호출할 경우, 자바 런타임은 가상 스레드를 일시 중지하고, 중지된 가상 스레드와 연관된 OS 스레드는 다른 가상 스레드를 위해 작업을 수행할 수 있습니다.
가상 스레드는 가상 메모리와 유사한 방식으로 구현됩니다. 운영 체제가 많은 메모리를 시뮬레이션하기 위해 제한된 RAM에 큰 가상 주소 공간을 매핑하는 것처럼, 자바 런타임은 많은 스레드를 시뮬레이션하기 위해 작은 수의 OS 스레드에 많은 가상 스레드를 매핑합니다.
플랫폼 스레드와 달리, 가상 스레드는 일반적으로 얕은 호출 스택을 가지며, 단일 HTTP 클라이언트 호출이나 단일 JDBC 쿼리와 같은 간단한 작업을 수행할 수 있습니다. 가상 스레드는 스레드 로컬 변수와 상속 가능한 스레드 로컬 변수를 지원하지만, 단일 JVM이 수백만 개의 가상 스레드를 지원할 수 있으므로 이들의 사용에 주의해야 합니다.
가상 스레드는 대부분의 시간을 차단 상태에서 보내며 I/O 작업의 완료를 기다리는 작업을 실행하는 데 적합합니다. 그러나 장시간 CPU를 집중적으로 사용하는 작업에는 적합하지 않습니다.
가상 스레드를 사용하는 이유
가상 스레드는 특히 많은 동시 작업을 포함하며, 이러한 작업들이 대부분의 시간을 대기하며 보내는 고성능 동시성 애플리케이션에서 사용됩니다. 서버 애플리케이션은 자원을 가져오는 등의 차단 I/O 작업을 수행하는 많은 클라이언트 요청을 처리하기 때문에 고성능 애플리케이션의 예시입니다.
가상 스레드는 더 빠른 스레드가 아닙니다; 플랫폼 스레드보다 코드를 더 빨리 실행하지 않습니다. 그들은 속도(낮은 지연 시간)가 아닌 규모(높은 처리량)를 제공하기 위해 존재합니다.
가상 스레드 생성 및 실행
Thread 및 Thread.Builder API는 플랫폼 스레드와 가상 스레드를 생성하는 방법을 제공합니다. java.util.concurrent.Executors 클래스는 각 작업마다 새로운 가상 스레드를 시작하는 ExecutorService를 생성하는 메소드도 정의합니다.
주제들
- Thread 클래스와 Thread.Builder 인터페이스를 사용하여 가상 스레드 생성
- Executors.newVirtualThreadPerTaskExecutor() 메소드를 사용하여 가상 스레드 생성 및 실행
- 멀티스레드 클라이언트 서버 예시
Thread 클래스와 Thread.Builder 인터페이스를 사용하여 가상 스레드 생성하기
Thread.ofVirtual() 메소드를 호출하여 가상 스레드를 생성하기 위한 Thread.Builder 인스턴스를 만듭니다.
다음 예제는 메시지를 출력하는 가상 스레드를 생성하고 시작합니다. 가상 스레드가 종료될 때까지 기다리기 위해 join 메소드를 호출합니다. (이렇게 하면 메인 스레드가 종료되기 전에 출력된 메시지를 볼 수 있습니다.)
Thread thread = Thread.ofVirtual().start(() -> System.out.println("Hello")); thread.join();
Thread.Builder 인터페이스를 사용하면 스레드 이름과 같은 일반적인 Thread 속성을 가진 스레드를 생성할 수 있습니다. Thread.Builder.OfPlatform 서브인터페이스는 플랫폼 스레드를 생성하는 반면, Thread.Builder.OfVirtual은 가상 스레드를 생성합니다.
다음 예제는 Thread.Builder 인터페이스를 사용하여 MyThread라는 이름의 가상 스레드를 생성합니다:
Thread.Builder builder = Thread.ofVirtual().name("MyThread"); Runnable task = () -> { System.out.println("Running thread"); }; Thread t = builder.start(task); System.out.println("Thread t name: " + t.getName()); t.join();
다음 예제에서는 Thread.Builder를 사용하여 두 개의 가상 스레드를 만들고 시작합니다.Thread.Builder builder = Thread.ofVirtual().name("worker-", 0); Runnable task = () -> { System.out.println("Thread ID: " + Thread.currentThread().threadId()); }; // name "worker-0" Thread t1 = builder.start(task); t1.join(); System.out.println(t1.getName() + " terminated"); // name "worker-1" Thread t2 = builder.start(task); t2.join(); System.out.println(t2.getName() + " terminated");
이 예에서는 다음과 유사한 출력을 인쇄합니다.Thread ID: 21 worker-0 terminated Thread ID: 24 worker-1 terminated
Executors.newVirtualThreadPerTaskExecutor() 메소드를 사용하여 가상 스레드 생성 및 실행하기
Executors는 스레드 관리와 생성을 나머지 애플리케이션과 분리할 수 있게 해줍니다.
다음 예제에서는 Executors.newVirtualThreadPerTaskExecutor() 메소드로 ExecutorService를 생성합니다. ExecutorService.submit(Runnable)이 호출될 때마다 새로운 가상 스레드가 생성되어 작업을 실행하게 됩니다. 이 메소드는 Future 인스턴스를 반환합니다. Future.get() 메소드는 스레드의 작업이 완료될 때까지 기다립니다. 따라서, 이 예제에서는 가상 스레드의 작업이 완료되면 메시지를 출력합니다.
try (ExecutorService myExecutor = Executors.newVirtualThreadPerTaskExecutor()) { Future<?> future = myExecutor.submit(() -> System.out.println("Running thread")); future.get(); System.out.println("Task completed"); // ...
멀티스레드 클라이언트 서버 예시
다음 예제는 두 개의 클래스로 구성되어 있습니다. EchoServer는 특정 포트에서 연결을 대기하고 각 연결에 대해 새로운 가상 스레드를 시작하는 서버 프로그램입니다. EchoClient는 서버에 연결하고 명령 줄에 입력된 메시지를 전송하는 클라이언트 프로그램입니다.
EchoClient는 소켓을 생성하여 EchoServer에 연결합니다. 사용자의 입력을 표준 입력 스트림에서 읽은 다음, 그 텍스트를 소켓을 통해 EchoServer로 전송합니다. EchoServer는 받은 텍스트를 다시 EchoClient에게 소켓을 통해 보내 줍니다. EchoClient는 서버에서 돌아온 데이터를 읽고 표시합니다. EchoServer는 각 클라이언트 연결마다 하나의 가상 스레드를 사용하여 여러 클라이언트를 동시에 처리할 수 있습니다.
public class EchoServer { public static void main(String[] args) throws IOException { 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(); // Accept incoming connections // Start a service thread 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); } } }
가상 스레드(virtual thread)와 고정 가상 스레드(pinned virtual thread) 스케줄링
운영 체제는 플랫폼 스레드(platform thread)가 실행되는 시기를 스케줄합니다. 그러나 자바 런타임은 가상 스레드(virtual thread)가 실행되는 시기를 스케줄합니다. 자바 런타임이 가상 스레드를 스케줄할 때, 가상 스레드를 플랫폼 스레드에 마운트합니다. 그 다음 운영 체제는 평소와 같이 해당 플랫폼 스레드를 스케줄합니다. 이 플랫폼 스레드를 캐리어 스레드(carrier thread)라고 합니다. 일부 코드를 실행한 후, 가상 스레드는 캐리어 스레드에서 언마운트할 수 있습니다. 이는 보통 가상 스레드가 차단 I/O 작업을 수행할 때 발생합니다. 가상 스레드가 캐리어 스레드에서 언마운트된 후, 캐리어 스레드는 자유 상태가 되어 자바 런타임 스케줄러가 다른 가상 스레드를 그 위에 마운트할 수 있습니다.
가상 스레드는 다음과 같은 상황에서 고정 가상 스레드(pinned virtual thread)로 고정되어 캐리어 스레드에서 언마운트할 수 없습니다:
- 가상 스레드가 동기화된 블록이나 메소드 내에서 코드를 실행할 때
- 가상 스레드가 네이티브 메소드나 외부 함수(외부 함수 및 메모리 API 참조)를 실행할 때
고정(pinning)은 애플리케이션의 정확성을 해치지 않지만, 확장성을 저해할 수 있습니다. 자주 실행되고 잠재적으로 오래 걸리는 I/O 작업을 보호하는 동기화된 블록이나 메소드를 재검토하고 java.util.concurrent.locks.ReentrantLock을 사용하여 고정을 자주하고 오래 지속되지 않도록 하세요.
작성중..'JAVA' 카테고리의 다른 글
Java의 Nested Classes (0) 2024.08.05 ObjectMapper와 MapStruct의 성능차이 (0) 2024.04.02 Java volatile 키워드 (1) 2023.01.05 AsyncRestTemplate PATCH 메서드 유효하지 않은 요청 (0) 2022.12.30 Memory leak - Thread dump 활용 (0) 2022.07.10