원문 (아래 리스트 순으로 번역되어 있음. Translated by Google Gemini) :


Java 스레드 vs. 가상 스레드 Pt.1#

면접 중에 한 면접관이 저에게 Java 스레드와 가상 스레드의 성능 차이에 대해 물었습니다. 저는 가상 스레드가 실제로는 JVM이 처리하는 경량 스레드이기 때문에 더 빠를 것이라고 답했지만, 둘 사이의 정확한 성능 차이가 궁금했습니다. 그래서 가상 스레드의 성능 향상을 확인하기 위해 간단한 벤치마크를 수행했습니다.

이것은 매우 간단한 테스트입니다. 저는 매우 많은 수의 스레드와 가상 스레드를 생성하며, 각각이 완료되는 데 5초가 걸리도록 했습니다. 그런 다음 앱 실행 시간을 계산합니다. 이 벤치마크에 사용한 Java 코드는 다음과 같습니다:

package info.behzadian.thread;

import java.util.concurrent.CountDownLatch;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        int numThreads = 100000;
        CountDownLatch latch = new CountDownLatch(numThreads);

        long start = System.currentTimeMillis();
        for(int i =0; i < numThreads; i++) {
            startThreads(latch);
        }
        latch.await(); // 모든 스레드가 끝날 때까지 대기

        long end = System.currentTimeMillis();
        System.out.println("Time taken with regular threads: " + (end - start) + "ms");

        latch = new CountDownLatch(numThreads); // 래치 재설정

        start = System.currentTimeMillis();
        for(int i =0; i < numThreads; i++) {
            startVirtualThreads(latch);
        }
        latch.await(); // 모든 가상 스레드가 끝날 때까지 대기

        end = System.currentTimeMillis();
        System.out.println("Time taken with virtual threads: " + (end - start) + "ms");
    }

    private static void startThreads(CountDownLatch latch) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    latch.countDown(); // 래치의 카운트 감소
                }
            }
        }).start();
    }

    private static void startVirtualThreads(CountDownLatch latch) {
        Thread.startVirtualThread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    latch.countDown(); // 래치의 카운트 감소
                }
            }
        });
    }
}

첫 번째 실행에서는 100,000개의 스레드 대 가상 스레드로 시작했으며 결과는 다음과 같습니다:

Time taken with regular threads: 54639ms
Time taken with virtual threads: 9477ms

보시다시피, 가상 스레드가 훨씬 빠릅니다. 일반 스레드 실행 시간의 약 17%를 차지합니다.

그런 다음, 200,000개의 스레드로 벤치마크를 시도했으며 결과는 다음과 같습니다:

Time taken with regular threads: 98614ms
Time taken with virtual threads: 12260ms

보시다시피, 스레드 수를 늘리면 가상 스레드가 훨씬 더 효과적으로 작동합니다! 일반 스레드 실행 시간의 약 12.4%를 차지합니다. 저는 16GB RAM이 장착된 Windows 11 노트북에서 Java 버전 21.0.2를 사용하고 있었습니다.

Java 스레드 vs. 가상 스레드 Pt.2#

이전 게시물(여기여기)에서 Java의 스레드와 Java의 가상 스레드 및 Kotlin의 코루틴 간의 매우 간단한 성능 벤치마크를 수행했습니다. 제 친구 Hossein은 더 나은 통찰력을 얻기 위해 메모리와 성능 사용량이 많은 작업으로 벤치마크를 수행해달라고 요청했습니다. 여기서는 벤치마크 테스트를 변경하여 각 작업 내에서 몇 초 동안 잠자기만 하는 대신, 이번에는 훨씬 더 높은 메모리와 CPU 집약적인 작업을 수행하려고 합니다.

첫 번째 라운드#

첫 번째 라운드에서는 1,000,000개의 정수 배열을 복사한 다음 배열을 반복하여 특정 값을 찾는 코드로 변경했습니다. 다음은 작업 실행 가능 코드입니다.

Runnable task = new Runnable() {
    @Override
    public void run() {
      try {
        boolean found = false;
        int[] numbersCopy = Arrays.copyOf(numbers, numbers.length);
        for (int i = 0; i < numbersCopy.length; i++) {
          if (numbersCopy[i] == 1000000) {
            found = true;
          }
        }
      } finally {
        latch.countDown(); // 래치 카운트 감소
      }
    }
  };

다음은 10,000개 스레드의 결과입니다.

일반 스레드 소요 시간: 5059ms
가상 스레드 소요 시간: 2478ms

가상 스레드가 더 나은 성능을 보입니다. 약 두 배 더 빠릅니다. 이제 50,000개 스레드로 시도하겠습니다.

일반 스레드 소요 시간: 17092ms
가상 스레드 소요 시간: 12289ms

가상 스레드의 성능은 스레드보다 여전히 좋지만 이전 실행보다는 떨어집니다. 100,000개 스레드로 테스트하여 결과를 확인해 보겠습니다.

일반 스레드 소요 시간: 34063ms
가상 스레드 소요 시간: 24385ms

가상 스레드가 여전히 더 나은 성능을 보입니다! 200,000개 스레드의 경우:

일반 스레드 소요 시간: 65082ms
가상 스레드 소요 시간: 48078ms

400,000개 스레드의 경우:

일반 스레드 소요 시간: 128880ms
가상 스레드 소요 시간: 95860ms

그리고 1,000,000개 스레드의 경우:

일반 스레드 소요 시간: 351020ms
가상 스레드 소요 시간: 253580ms

다음은 벤치마크 차트입니다.

https://blog.behzadian.info/images/Thread-Duration-and-Virtual-Threads-Duration.png

그리고 다음은 전체 소스 코드입니다.

package info.behzadian;

import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.CountDownLatch;

public class Main {

  public static void main(String[] args) throws InterruptedException {
    Main main = new Main();
    main.startTest();
  }

  CountDownLatch latch;
  private int[] numbers;

  private void startTest() throws InterruptedException {
    int numThreads = 1_000_000;
    latch = new CountDownLatch(numThreads);
    numbers = createRandomNumbersArray();

    long start = System.currentTimeMillis();
    for(int i =0; i < numThreads; i++) {
      new Thread(task).start();
    }
    latch.await(); // 모든 스레드가 완료될 때까지 대기
    long end = System.currentTimeMillis();
    System.out.println("Time taken with regular threads: " + (end - start) + "ms");

    latch = new CountDownLatch(numThreads); // 래치 초기화
    start = System.currentTimeMillis();
    for(int i =0; i < numThreads; i++) {
      Thread.startVirtualThread(task);
    }
    latch.await(); // 모든 가상 스레드가 완료될 때까지 대기
    end = System.currentTimeMillis();
    System.out.println("Time taken with virtual threads: " + (end - start) + "ms");
  }

  Runnable task = new Runnable() {
    @Override
    public void run() {
      try {
        boolean found = false;
        int[] numbersCopy = Arrays.copyOf(numbers, numbers.length);
        for (int i = 0; i < numbersCopy.length; i++) {
          if (numbersCopy[i] == 1000000) {
            found = true;
          }
        }
      } finally {
        latch.countDown(); // 래치 카운트 감소
      }
    }
  };

  private static int[] createRandomNumbersArray() {
    int[] numbers = new int;
    Random random = new Random();
    for (int i = 0; i < numbers.length; i++) {
      numbers[i] = 1 + random.nextInt(Integer.MAX_VALUE - 1);
    }

    return numbers;
  }
}

두 번째 라운드#

이제 CPU 집약적인 작업에서 스레드와 가상 스레드 간의 성능 비교를 확인하려고 합니다. 크기 1,000의 무작위로 생성된 long 값 배열을 생성한 다음 각 실행에서 이 배열을 반복하여 숫자가 회문수인지 확인합니다.

다음은 새로운 작업 실행 가능 코드입니다.

  Runnable task = new Runnable() {
    @Override
    public void run() {
      try {
        int palindromeCount = 0;
        for (long num : numbers) {
          if (isPalindrome(num)) {
            palindromeCount++;
          }
        }
      } finally {
        latch.countDown(); // 래치 카운트 감소
      }
    }
  };

그리고 다음은 long 숫자가 회문수인지 확인하는 함수입니다.

  public boolean isPalindrome(long num) {
    long reversed = 0, remainder, original = num;
    while (num != 0) {
      remainder = num % 10;
      reversed = reversed * 10 + remainder;
      num /= 10;
    }
    return original == reversed;
  }

다음은 10,000개 스레드의 결과입니다.

일반 스레드 소요 시간: 1003ms
가상 스레드 소요 시간: 124ms

이제 50,000개 스레드로 시도하겠습니다.

일반 스레드 소요 시간: 3655ms
가상 스레드 소요 시간: 418ms

100,000개 스레드로 테스트하여 결과를 확인해 보겠습니다.

일반 스레드 소요 시간: 6560ms
가상 스레드 소요 시간: 805ms

200,000개 스레드의 경우:

일반 스레드 소요 시간: 14899ms
가상 스레드 소요 시간: 1891ms

400,000개 스레드의 경우:

일반 스레드 소요 시간: 26038ms
가상 스레드 소요 시간: 3242ms

그리고 1,000,000개 스레드의 경우:

일반 스레드 소요 시간: 65596ms
가상 스레드 소요 시간: 9821ms

다음은 벤치마크 차트입니다.

https://blog.behzadian.info/images/Thread-Duration-and-Virtual-Threads-Duration-CPU.png

CPU 처리가 필요한 작업의 경우 가상 스레드가 스레드보다 더 나은 성능을 보인다는 것이 매우 명확합니다. 그리고 다음은 전체 소스 코드입니다.

package info.behzadian;

import java.util.Random;
import java.util.concurrent.CountDownLatch;

public class Main {

  public static void main(String[] args) throws InterruptedException {
    Main main = new Main();
    main.startTest();
  }

  CountDownLatch latch;
  long[] numbers;

  private void startTest() throws InterruptedException {
    int numThreads = 10_000;
    latch = new CountDownLatch(numThreads);
    numbers = createRandomLongArray();

    long start = System.currentTimeMillis();
    for(int i =0; i < numThreads; i++) {
      new Thread(task).start();
    }
    latch.await(); // 모든 스레드가 완료될 때까지 대기
    long end = System.currentTimeMillis();
    System.out.println("Time taken with regular threads: " + (end - start) + "ms");

    latch = new CountDownLatch(numThreads); // 래치 초기화
    start = System.currentTimeMillis();
    for(int i =0; i < numThreads; i++) {
      Thread.startVirtualThread(task);
    }
    latch.await(); // 모든 가상 스레드가 완료될 때까지 대기
    end = System.currentTimeMillis();
    System.out.println("Time taken with virtual threads: " + (end - start) + "ms");
  }

  Runnable task = new Runnable() {
    @Override
    public void run() {
      try {
        int palindromeCount = 0;
        for (long num : numbers) {
          if (isPalindrome(num)) {
            palindromeCount++;
          }
        }
      } finally {
        latch.countDown(); // 래치 카운트 감소
      }
    }
  };

  private long[] createRandomLongArray() {
    long[] numbers = new long[1_000];
    Random random = new Random();

    for (int i = 0; i < numbers.length; i++) {
      numbers[i] = 1 + random.nextLong(Long.MAX_VALUE - 1);
    }

    return numbers;
  }

  public boolean isPalindrome(long num) {
    long reversed = 0, remainder, original = num;
    while (num != 0) {
      remainder = num % 10;
      reversed = reversed * 10 + remainder;
      num /= 10;
    }
    return original == reversed;
  }

}

자바 가상 스레드 성능 대 코틀린 코루틴#

이전 게시물에서 저는 자바 스레드와 자바 가상 스레드의 성능을 비교하기 위한 간단한 벤치마크를 수행했습니다. 이 게시물에서는 자바 가상 스레드를 코틀린 코루틴과 비교하고자 합니다.

이것은 매우 간단한 테스트입니다. 매우 많은 수의 코루틴을 생성하고, 각 코루틴은 5초가 걸려 완료됩니다. 그런 다음 앱 실행 시간을 계산합니다.

다음은 이 벤치마크에 사용한 코틀린 코드입니다:

package info.behzadian

import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking


fun main() = runBlocking {
    val startTime = System.currentTimeMillis()

    val jobs = List(100000) {
        launch {
            delay(5000L)
        }
    }

    jobs.forEach { it.join() }

    val endTime = System.currentTimeMillis()

    println("Total running time: ${endTime - startTime} ms")
}

첫 번째 실행에서 100,000개의 코루틴으로 시작했으며 결과는 다음과 같습니다:

총 실행 시간: 5302 ms

이 결과를 자바 가상 스레드(9477ms)와 비교하면 훨씬 빠르다는 것을 알 수 있습니다! 다음으로 200,000개의 코루틴으로 시도했으며 결과는 다음과 같습니다:

총 실행 시간: 5415 ms

놀랍습니다! 실행 중인 코루틴 수를 두 배로 늘렸는데도 100,000개의 코루틴에 대한 오버헤드는 단 100ms에 불과합니다! 이것을 자바 가상 스레드(12260ms)와 비교하면 의심할 여지 없이 승자는 코틀린 코루틴이라는 것을 알 수 있습니다!

저는 16GB RAM을 탑재한 Windows 11 노트북에서 Kotlin 버전 1.9.23과 Coroutines 버전 1.8.0을 사용했습니다.