프로그래밍/Java

Java21 Virtual Thread

개발정리 2024. 7. 6. 16:55

1. Java 21에서 가상 스레드를 왜 도입하였을까.


 

1) IO 중심 작업에서 처리량(성능)을 늘리기 위함

- 대표적인 IO 중심 작업은 DB를 사용하는 웹 서버를 예로 들수 있다. 

- DB를 사용하는 웹 서버는 DB와 통신하기 위해 IO를 사용한다.

- 즉, IO 중심이 많은 서버에서 처리량을 늘리기 위해 나온 것이 가상 스레드다. 

 

2) 트래픽이 많음 + IO가 많이 발생함 + 요청마다 쓰레드를 만드는 방식

- 이 방식을 사용하게되면 스레드가 메모리를 사용하기 때문에 많은 메모리가 사용된다.

- IO가 발생하면 응답 대기 (blocking) 되는 시간, 컨텍스트 스위칭에 따른 CPU 낭비가 발생함.

- 위와 같은 문제 때문에 스레드를 늘리고 싶어도 늘리지 못한다. 그리고 서버들은 스레드를 pool로 관리한다. 하지만 성능은 일정 수준까지 밖에 못올라간다는 단점이 있다. 

 

다시 말해, 스레드를 사용한다는 것은 많은 메모리가 사용되고 컨텍스트 스위칭에 따른 CPU 낭비가 발생한다. 그렇기 때문에 스레드를 함부로 늘리지 못하였고 스레드 풀로 스레드를 관리하였다. 스레드 풀을 통해 스레드를 관리한다는 것은 성능을 일정 수준까지 밖에 못 올린다는 말과 같다. 

 

3) 보통 위와 같은 해결 방법은 두 가지 중 하나를 사용한다.

- 비동기 IO + 적은 스레드 사용 (ex. reactor)

- 위 방법은 코드 가독성과 동작방식이 복잡하다. 

- 경량 스레드 + IO 연동 (ex. gorutine)

- go 언어에 gorutine이 이 방법인데, Java에도 이 방법이 들어온 것이다.

 

 

2. 가상 스레드 목적


 

가상 스레드 문서를 통해 목적을 확인해 볼 수 있었다. 

 

1) 요청 당 스레드를 발생시키는 구조를 갖는 서버 어플리케이션의 HW 최적 사용 

즉, 하드웨어를 최대한 사용할 수 있게 (성능을 최대로 낼 수 있게) 하는 것이 목적.

 

2) 최소 변경으로 기존 코드에 가상 스레드 적용 

 

 

3. 가상 스레드 사용 방법


 

 

 

궁금증 

1) Thread와 ExcutorService의 차이를 찾아보자. 

 

 

4. 가상 스레드는 어떤 방식으로 스케줄링이 될까?


 

그 동안 스레드라 불렀던 것은 '플랫폼 스레드'이다. 그리고 플랫폼 스레드와 OS 스레드는 1:1로 매핑된다. 그리고 가상 스레드를 실행할 때 사용할 플랫폼 스레드를 Pool 안에 만들어 놓는다.

 

 

Pool에 생성되는 플랫폼 스레드는 CPU 코어 개수~256개까지 상황에 맞게 작동이 된다.

 

 

가상 스레드는 플랫폼 스레드와 m:n 으로 연결이 된다. 하나의 플랫폼 스레드가 여러 개의 가상 스레드를 실행할 수 있다. 예를들어, 1번 가상 스레드 실행중에 IO가 발생하여 멈추면 2번 가상 스레드를 실행한다. 반대로, 하나의 가상 스레드를 여러 플랫폼 스레드가 나눠서 실행할 수도 있다. 그래서 m:n의 관계를 갖게되는 것이다. 

 

 

이 때 가상 스레드를 실행하고 있는 플랫폼 스레드를 '캐리어 스레드'라 부른다.  

 

 

정리하자면,

 

1) 기존 자바 스레드에서 불리던 스레드는 '플랫폼 스레드'이다. 

2) 플랫폼 스레드는 OS 스레드와 1:1로 매핑된다. 

3) 플랫폼 스레드는 Pool안에 만들어 놓는다.

4) Pool에 생성되는 플랫폼 스레드는 CPU개수~256개까지 상황에 맞게 작동된다.

5) 가상 스레드는 플랫폼 스레드와 m:n으로 연결된다.

6) 가상 스레드를 실행하고 있는 플랫폼 스레드를 캐리어 스레드라 부른다.

 

 

5. 가상 스레드에서 중요한 지점은 IO 이다. 


 

기존 플랫폼 스레드는 IO Blocking이 발생하면 대기 상태가 되었다. 즉 스레드 하나가 낭비된 것이다. 가상 스레드는 IO Blocking이 발생하면 캐리어 스레드가 다른 가상 스레드를 실행한다. 

 

 

즉, 캐리어 스레드가 플랫폼 스레드이니까 플랫폼 스레드와 OS 스레드를 블록킹하지 않는것이다. 이어서 캐리어 스레드는 IO 처리가 가능해지면 가상 스레드를 이어서 실행한다.

 

 

코드 자체는 동기로 작성되어 있는데, 실제 동작은 비동기처럼 동작을 하게된다. 그러면서 성능상 이점이 발생한다.

 

 

성능상 이점

1) 스케줄링(컨텍스트 스위칭) 부하: 가상 스레드 < 플랫폼 스레드

- 플랫폼 스레드의 컨텍스트 스위칭 비용이 가상 스레드의 컨텍스트 스위칭 비용보다 훨씬 더 크다. 

2) 메모리 사용: 가상 스레드 < 플랫폼 스레드

3) IO 블록킹에 따른 대기 시간 낭비: 가상 스레드 < 플랫폼 스레드  

 

 

IO 블록킹이 발생하면 캐리어 스레드가 같이 대기하는 것이 아닌 다른 가상 스레드를 실행한다. 이는 IO 블록킹에 따른 대기시간 낭비를 줄여준다. 결과적으로 동일한 자원을 사용한다면 가상 스레드가 더 많은 처리량을 보여줄 수 있다. 

 

 

6. 가상 스레드 사용 시 주의사항. Pinned 


 

Pinned는 가상 스레드가 캐리어 스레드에 고정되는 것을 말한다. 이 경우 캐리어 스레드가 다른 가상 스레드를 실행하지 못하는 상황이 발생한다. 

 

 

Pinned는 언제 발생할까.

 

1) Synchronized 블록에서 IO 블록킹이 발생하면 JVM은 가상 스레드를 캐리어 스레드에 고정시킨다. 그러므로 캐리어 스레드는 다른 가상 스레드를 실행할 수 없게 되어버린다. 해결법은 Synchronized 블록 대신 Lock을 사용하여 피할 수 있다.

 

2)네이티브 메서드 또는 foreign 함수를 사용할 때 발생한다. 

 

 

 

7. 기타


 

1) 자바에서 Thread Local은 중요하다. 가상 스레드 역시 Thread Local 지원한다. 그러므로 기존 도구를 그대로 사용할 수 있다. 주의사항으로 가상 스레드는 정말 많이 만들 수 있기 때문에 주의해서 사용해야 한다.

 

가상 스레드마다 Thread Local을 사용하면 Thread Local 마다 메모리가 사용되고 그게 100만개라면 가상스레드의 Thread Local 메모리가 아무리 작더라도 성능 저하가 나타날 수 있다. 

 

 

2) 가상 스레드는 풀링이 필요 없다. 필요할 때마다 만들어서 사용하면 된다.

 

 

8. 정리


1) 코드 수정없이 성능을 늘릴 수 있다. 

- 단, synchronized 블록에서 IO를 하면 안된다. (Java 21기준) 그러므로 사용하는 라이브러리 확인이 필요하다.

- CPU를 주로 사용하는 작업에는 효과가 없다. 네트워크 IO 작업일 때 효과가 있다. CPU 연산이 많은 작업에서 가상 스레드를 늘려놓으면 CPU를 나눠먹기 하는 것이므로 성능이 떨어지게 된다.

 

2) JDBC 드라이버에서 Pinned 스레드가 발생하지 않게 코드를 수정하는 날이 얼른 오길!

 

 

 

https://www.youtube.com/watch?v=srpOD6WIasM