프로그래밍/Java

Java Virtual Thread (5), Pinned Virtual Thread

개발정리 2024. 6. 23. 14:17

Pinned Virtual Thread를 직역하면 고정된 가상 스레드이다. 

 

 

Virtual Thread는 blocking 작업이 되면(blocking 메서드를 호출하면) 플랫폼 스레드가 언마운트되고 다른 Virtual Thread가 플랫폼 스레드를 사용함으로써 효율은 높이는 방식인데 그것을 못하게 하는 것이 Pinned Virtual Thread 이다.

 

 

즉, virtual thread 는 Synchronized block 혹은 Native 메서드와 함께 사용하는 것은 피해야한다.

 

 

1) Native 메서드

 

 

Object 클래스를 보면 hascode 라는 메서드가 있다. 이는 native로 설정이 되어 있으며 body 값이 없다. native 로 적혀져 있는 것은 c, c++ 같은 native 언어로 만들어진 라이브러리에서 실제 body가 실행되는 것이고 java에 선언된 hascode 메서드는 해당 라이브러리 코드 호출을 의미한다.

 

 

이 처럼 native로 선언한 것은 제어권이 넘어간 것이기 때문에  java에서 스케줄링해주는 virtual thread 스케줄링이 불가능하다. 만약, native 메서드를 호출했을 때 거기서 blocking 이 된다면 이는 성능저하로 이어진다. 

 

 

 

 

2) Synchronized block

 

lock 동기화를 위해 Synchronized block 을 사용한다.

private final Runnable runnable = new Runnable() {
    @Override
    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 block 안에 sleep이 들어갈 경우는 어떤 문제가 발생할까,

 

원래는 sleep이 들어가면 언마운트되어서 새로운 virtual thread와 마운트가 되는데, Synchronized block 안에 sleep이 들어갈 경우 새로운 virtual thread와 마운트 되는것이 불가능해진다.

 

 

 

@Slf4j
public class Pinning {

    private final ReentrantLock lock = new ReentrantLock();

    // -Djdk.tracePinnedThreads=full  or -Djdk.tracePinnedThreads=short 를 통해  detect
    private final Runnable runnable = new Runnable() {
        @Override
        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());
            }

        }
    };


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

        platform();
        //virtual();

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

    private static void virtual() {
        ThreadFactory factory = Thread.ofVirtual().name("myVirtual-", 0).factory();
        try (ExecutorService executorService = Executors.newThreadPerTaskExecutor(factory)) {
            for (int i = 0; i < 20; i++) {
                Pinning pinning = new Pinning();
                executorService.submit(pinning.runnable);
            }
        }
    }

    private static void platform() {
        try (ExecutorService executorService = Executors.newFixedThreadPool(20)) {
            for (int i = 0; i < 20; i++) {
                Pinning pinning = new Pinning();
                executorService.submit(pinning.runnable);
            }
        }
    }
}

 

실제로 위 코드를 실행하여 virtual thread와 platform thread 의 실행속도를 비교해보면 2배 가량 차이가 난다.

(virtual thread 10s, platform thread 5s) virtual thread 효율이 전혀 나오지 않는 것이다.

 

 

IO Burst, IO Bound 작업이 많이 있더라도 Synchronized block 안에 있다면 pinned virtual thread가 되기 때문에 효율이 나오지 못하는 것이다.

 

 

MySQL 드라이버, JDBC 드라이버에서  Synchronized block이 사용되고 그 안에서 sleep 까지 사용되고 있다면 vitual thread 효율이 나오지 않을 것이다.

 

 

 

 

 

 Synchronized block이 third party library에서 사용되고 있는지 어떻게 알 수 있을까?

 

// -Djdk.tracePinnedThreads=full  or -Djdk.tracePinnedThreads=short 를 통해  detect

 

 

 

 

위 옵션을 넣어 실행해보면 어느 지점에 pinned virtual thread가 되었는지 확인할 수 있다.

 

 

 

 

 

이에 대해, Java에서는 Synchronized lock을 대체할 수 있는 ReentrantLock을 사용할 것을 권장하고 있다. 

(ReentrantLock 사용 시 pinned virtual thread가 되지 않는다.)

 

 

...

 

 

정리

 

1) virtual thread 사용 시 native 메서드, Synchronized block 과 함께 사용하지 말자. 

2) 위 경우 pinned virtual thread가 발생한다.

3) pinned virtual thread 는 sleep 시 virtual thread 와 마운트가 되지 않아 성능저하로 이어진다. 

4) Synchronized block  대신 ReentrantLock을 사용하자.