프로그래밍/Java

Java Virtual Thread (8), Virtual Thread와 Platform Thread 성능비교

개발정리 2024. 6. 23. 15:33
 

1) IO 바운드 작업 비교

 

private static final Runnable ioBoundRunnable = new Runnable() {
    @Override
    public void run() {
        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());
    }
};
private static void platformThreadWithIoBound() {
    try (ExecutorService executorService = Executors.newFixedThreadPool(10000)) {
        for (int i = 0; i < 10000; i++) {
            executorService.submit(ioBoundRunnable);
        }
    }
}

 

일반 플랫폼 스레드 1만개를 만들고, 1만번 돌리면서 5초 sleep 을 설정하자. 실행해보면 대략 6.5 s 가 소요된다.

 

 

 

private static void virtualThreadWithIoBound() {
    ThreadFactory factory = Thread.ofVirtual().name("myVirtual-", 0).factory();
    try (ExecutorService executorService = Executors.newThreadPerTaskExecutor(factory)) {
        for (int i = 0; i < 10000; i++) {
            executorService.submit(ioBoundRunnable);
        }
    }
}

 

가상 스레드를 돌리면 동일한 조건으로 실행해보면 5.3s 가 소요된다. 

 

 

IOBound 작업에서는 가상 스레드가 조금 더 빠르다는 결론을 낼 수 있다. 그리고 이를 2만번 돌렸을 때, 그리고 sleep 설정을 올린다면 성능은 압도적으로 벌어진다. 스레드가 훨씬 많아질수록 가상 스레드 성능이 훨씬 좋아지는 것이다.  

 

 

 

2) CPU 바운드 작업 비교

 

private static final Runnable cpuBoundRunnable = new Runnable() {
    @Override
    public void run() {
        log.info("1) run. thread: " + Thread.currentThread());
        long sum = 0;
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            sum = sum + i;
        }
        log.info("2) run. sum: " + sum + " , thread: " + Thread.currentThread());
    }
};
private static void platformThreadWithCpuBound() {
    try (ExecutorService executorService = Executors.newFixedThreadPool(100)) {
        for (int i = 0; i < 100; i++) {
            executorService.submit(cpuBoundRunnable);
        }
    }
}

 

 CPU 바운드 작업을 확인해보자. 플랫폼 스레드의 경우 4.4s 가 걸렸다.  

 

 

private static void virtualThreadWithCpuBound() {
    ThreadFactory factory = Thread.ofVirtual().name("myVirtual-", 0).factory();
    try (ExecutorService executorService = Executors.newThreadPerTaskExecutor(factory)) {
        for (int i = 0; i < 100; i++) {
            executorService.submit(cpuBoundRunnable);
        }
    }
}

 

가상 스레드도 똑같이 100개를 만들어 돌리면 4.7s 가 나온다. 

 

 

 

이를 통해 알 수 있는 것은 CPU 바운드 작업의 경우 Virtual Thread를 사용하는 장점이 하나도 없어진다.

 

플랫폼에서 계속 CPU를 사용하기 때문에 Virtual Thread는 슬립이나 블록킹이 됐을 때 새로운 Thread 를 마운트 하지 못한다.

(Virtual Thread의 장점은 sleep 혹은 blocking 되었을 때 새로운 Thread 를 마운트하고 기존 Thread를 언마운트하여 효율성을 높이는 것이다.) 

 

 

오히려 가상 스레드를 만들고 스케줄링 해주는 과정에서 오히려 시간이 더 걸린 것이다. 

 

 

@Slf4j
public class PerformanceTest {

    private static final Runnable ioBoundRunnable = new Runnable() {
        @Override
        public void run() {
            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());
        }
    };

    private static final Runnable cpuBoundRunnable = new Runnable() {
        @Override
        public void run() {
            log.info("1) run. thread: " + Thread.currentThread());
            long sum = 0;
            for (int i = 0; i < Integer.MAX_VALUE; i++) {
                sum = sum + i;
            }
            log.info("2) run. sum: " + sum + " , thread: " + Thread.currentThread());
        }
    };

    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        log.info("1) main. thread: " + Thread.currentThread());

//        platformThreadWithIoBound();
//        virtualThreadWithIoBound();

//        platformThreadWithCpuBound();
//        virtualThreadWithCpuBound();

        log.info("2) main. time: " + (System.currentTimeMillis()-startTime) + " , thread: " + Thread.currentThread());
    }

    private static void platformThreadWithIoBound() {
        try (ExecutorService executorService = Executors.newFixedThreadPool(10000)) {
            for (int i = 0; i < 10000; i++) {
                executorService.submit(ioBoundRunnable);
            }
        }
    }

    private static void virtualThreadWithIoBound() {
        ThreadFactory factory = Thread.ofVirtual().name("myVirtual-", 0).factory();
        try (ExecutorService executorService = Executors.newThreadPerTaskExecutor(factory)) {
            for (int i = 0; i < 10000; i++) {
                executorService.submit(ioBoundRunnable);
            }
        }
    }

    private static void platformThreadWithCpuBound() {
        try (ExecutorService executorService = Executors.newFixedThreadPool(100)) {
            for (int i = 0; i < 100; i++) {
                executorService.submit(cpuBoundRunnable);
            }
        }
    }

    private static void virtualThreadWithCpuBound() {
        ThreadFactory factory = Thread.ofVirtual().name("myVirtual-", 0).factory();
        try (ExecutorService executorService = Executors.newThreadPerTaskExecutor(factory)) {
            for (int i = 0; i < 100; i++) {
                executorService.submit(cpuBoundRunnable);
            }
        }
    }
}

 

 

 ...

 

 

정리 

 

1) CPU 바운드 작업에서는 플랫폼 스레드를 사용하자.

2) IO 바운드 작업에서는 가상 스레드를 사용하자.  

3) 어플리케이션 프로세스에서 어떤 곳은 CPU 바운드 작업이고, 어떤 곳은 IO 바운드 작업이라면 그 영역에 맞게 플랫폼 스레드 혹은 가상 스레드를 적용하면 된다. 

4) 한 프로세스에서 특정 스레드만을 고집할 필요는 절대 없다.