Virtual Thread (Pure Java21)

차_현·2024년 11월 22일
1

  • 일반 Thread
/**
* 기본 Thread
*/
Thread thread = new Thread(runnable);
thread.run();


  • 단일 Virtual Thread
/**
* Virtual Thread
*/
Thread virtual_thread = Thread.ofVirtual().name("Virtual-Thread").start(runnable);
virtual_thread.join();

  • ForkJoinPool : Virtual Thread는 Platform Thread에 붙어서 실행되기 때문에, Platform Thread의 정보를 알려주는 것이다. (아래에서 좀 더 다뤄볼 예정)
  • Virtual Thread는 기본적으로 Deamon Thread이기 때문에 join이 필요하다.

  • 일반 Thread Pool
package sample.virtual_thread.pure;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class VirtualThreadExecutorCreation {

    private static final Runnable runnable = new Runnable() {
        @Override
        public void run() {
            log.info("[1] Run Thread: {}", Thread.currentThread());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            log.info("[2] Run Thread: {}", Thread.currentThread());
        }
    };

    public static void main(String[] args) throws InterruptedException {
        log.info("[1] main thread : {}", Thread.currentThread());

        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            executorService.submit(runnable);
        }

        log.info("[2] main thread : {}", Thread.currentThread());
    }
}

여기서 실행결과의 중간에 보이는 main thread를 executorService 부분이 다 끝나고 실행되게 해주려면

executorService.close(); 을 추가하면 된다.

그리고 ExecutorService를 보면 AutoClosable을 extend하고 있는 형태이다.

AutoCloseableJava 7에서 도입된 인터페이스로, try-with-resources 구문을 사용할 때 자원을 자동으로 닫을 수 있게 해주는 것이다. AutoCloseable을 구현한 객체는 try-with-resources 구문을 통해 자동으로 close() 메서드가 호출되어 자원을 안전하게 해제할 수 있습니다.

그래서 이전에 작성하였던 코드를 아래와 같이 바꾸면, 굳이 수동으로 executorService.close() 를 추가하지 않아도 된다.

[이전코드]
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
	 executorService.submit(runnable);
}
executorService.close();

[수정된 코드]
try (ExecutorService executorService = Executors.newFixedThreadPool(10)) {
		for (int i = 0; i < 10; i++) {
				executorService.submit(runnable);
		}
}

이렇게 바꿀 수 있다.

  • Virtual Thread Pool

그러면, 이제 VirtualThread에서 사용하는 방법은 간단하다.

ThreadFactory factory = Thread.ofVirtual().name("Virtual_Thread", 0).factory();
try (ExecutorService executorService = Executors.newThreadPerTaskExecutor(factory)) {
		for (int i = 0; i < 100; i++) {
				executorService.submit(runnable);
    }
}

100개의 Virtual Thread를 생성한 코드이다. 또한, 각 Virtual Thread 별로 이름을 부여해서 고유 번호를 로그를 통해 확인하게 하였다. 이때 ThreadFactory를 이용할 수 있다.

ThreadFactory는 새로운 스레드를 생성하는 방식을 커스터마이즈할 수 있는 인터페이스이다. Java의 java.util.concurrent 패키지에 포함되어 있으며, 기본 스레드 생성 방식이 아닌 사용자 정의 방식으로 스레드를 생성하고 싶을 때 사용할 수 있다.

  • 유의할 점
    • Virtual Thread는 가벼운 성격의 Thread(한번 쓰고 버리는?)이기 때문에 Thread Pool을 만들 필요가 없다.

      private static void antiPattern_first() {
      	ThreadFactory factory = Thread.ofVirtual().name("Virtual_Thread", 0).factory();
      	try (ExecutorService executorService = Executors.newFixedThreadPool(1, factory)) {
      			for (int i = 0; i < 100; i++) {
      					executorService.submit(runnable);
      			}
      	}
      }
    • 이것도 위와 똑같은 의미의 코드이다.

      private static void antiPattern_first() {
      	ThreadFactory factory = Thread.ofVirtual().name("Virtual_Thread", 0).factory();
      	try (ExecutorService executorService = Executors.newSingleThreadExecutor(factory)) {
      			for (int i = 0; i < 100; i++) {
      					executorService.submit(runnable);
      			}
      	}
      }

Virtual Threads는 제한된 고정 스레드 풀이나 단일 스레드 Executor로 묶어서 사용하는 것이 적합하지 않다는 것이다.
대신 Virtual Thread를 사용할 때는 Executors.newThreadPerTaskExecutor(factory) 와 같은 방식을 사용해 각 작업이 개별 Virtual Thread에서 실행되도록 하는 것이 좋다.


  • ForkJoinPool (Java 7부터 존재한 Class이다.)

  • ForkJoinPool은 Java 7에서 도입된 병렬 처리용 스레드 풀로, 작업을 작은 단위로 나누어 병렬로 처리할 수 있게 해주는 것이다.

아래는 ForkJoinPool이 쓰이는 것을 보기 위한 예제 코드이다.

package sample.virtual_thread.pure;

import java.util.List;
import java.util.Optional;

public class ForkJoinPoolSample {
    public static void main(String[] args) {
        List<Integer> list = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        Optional<Integer> optional = list.parallelStream()
                .filter(integer -> {
                    System.out.println("integer : " + integer + ", thread : " + Thread.currentThread() + ", deamon : " + Thread.currentThread().isDaemon());
                    return integer % 2 == 0;
                })
                .findAny();

        System.out.println(optional.get());
    }
}

일반 stream()이 아닌, parallelStream() 즉 병렬 Stream을 사용했을 때의 결과이다. 일반 Stream을 사용했을 때와 다르게, 결과가 계속 바뀐다.(병렬이니깐 여러 Thread에서 돌기 때문에)

  • Java에서는 사용자가 별도의 Thread를 만들지 않았을때, Thread Pool이 필요하다면 ForkJoinPool이라는 Thread Pool을 이용해서 Thread를 할당시켜준다.
  • 그리고 실행결과의 5번째 줄을 보면 Main Thread는 Daemon Thread가 아님을 확인할 수 있다.(ForkJoinPool은 다 Deamon Thread이다. 즉, ForkJoinPool은 Java가 필요하면 만들어주는 것이며, 이것이 Daemon Thread로 동작하고 있다는 소리이다.)

  • Pinned Virtual Thread(고정된 Virtual Thread)

아래는 Pinned Virtual Thread에 대한 Oracle 문서이다.

https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html#GUID-704A716D-0662-4BC7-8C7F-66EE74B1EDAD

  • 원래 Virtual Thread는 blocking method를 호출하면 Platform Thread와 Unmount되고, 다른 Virtual Thread가 Platform Thread를 사용하면서 효율을 높인다.
  • 그런데 Pinned Virtual Thread는 이것을 못하게 하는 방식이다. 그럼 언제 이것을 못하게 하냐? 아래 2가지의 경우라고 한다.

  1. synchronized Block안에서 호출이 되거나
public void run() {
            synchronized (this) {
                log.info("[1] Run Thread: {}", Thread.currentThread());
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                log.info("[2] Run Thread: {}", Thread.currentThread());
            }
        }
  • synchronized (this): 이 블록은 synchronized 키워드를 사용하여 동기화된 코드 블록을 만든다.
    동기화된 블록에서 실행될 때 Virtual Thread는 Pinned 상태가 되는데, 즉 해당 Virtual Thread는 현재 사용 중인 플랫폼 스레드에서 언마운트되지 않고 고정되는 것이다.
  • Thread.sleep(5000): Virtual Thread가 5초 동안 대기하게 된다. 일반적으로 Virtual Thread는 Blocking 작업 중에 Platform Thread에서 Unmount될 수 있지만, synchronized Block 안에서는 Unmount 되지 않고 Platform Thread에 고정(Pinned)된 상태로 유지된다.
  1. Native 메소드를 호출할 때
  • Object Class를 보면 hashcode라는 메소드가 있다. 이 hashCode 메소드를 보면 native라는 키워드가 적혀있는 것을 볼 수 있다. native 키워드는 Java에서 네이티브 메서드를 정의할 때 사용되는데, native 메서드는 Java가 아닌 다른 언어로 작성된 메서드를 호출할 때 사용되며, 주로 C나 C++와 같은 언어로 작성된 코드를 호출하는 데 쓰인다.

이어서 작성중...

1개의 댓글

comment-user-thumbnail
2024년 12월 3일

친절하면서도 섬세한 설명...

답글 달기

관련 채용 정보