티스토리 뷰
기본기는 가볍게 정리해본다.
프로세스
- 프로세스는 운영체제에 의해 실행되는 포로그램이다.
- code, data, heap, stack 메모리 공간을 갖는다.
- stack은 지역변수, heap은 메모리 등을 사용하게 되는 동적 할당의 영역이다.
- code, data는 정적 할당의 영역이다.
스레드
- 스레드는 프로세스의 작업을 수행하는 최소 실행 단위이다.
- 스레드는 프로세스의 code, data, heap은 공유자원으로 사용한다.
- 스레드는 개별 stack을 갖는다.
CPU
- 스레드는 운영체제 스케쥴러에 의해 관리되는 CPU의 최소 실행 단위이다.
- 스레드가 CPU를 선점하게된다.
- CPU를 스레드가 선점할 때 문맥(레지스터, 캐시 등이 교체되는) 교환이 발생한다. (=컨텍스트 스위칭)
동시성 / 병렬성 개념을 정리해보자.
동시성
- 동시성은 CPU가 한꺼번에 여러개의 작업을 매우 빠르게 번갈아가면서 하는것이다.
- 타임 슬라이스 기법으로 여러 작업을 매우 빠르게 교체해가며 처리한다.
우리는 이것을 통해 여러 프로그램을 동시에 사용하고 있다는 착각에 빠진다. - 만약 해야할 작업이 2개인데 CPU 코어는 1개인 경우 동시성이 발생한다.
즉, 작업이 CPU 수보다 많은 경우에 시분할(Time Slicing)을 통한 동시성이 발생한다. - 동시성이 없다면, 작업은 순차처리하게 된다.
병렬성
- 복수의 CPU가 동시에 많은 일을 수행하는 것이다.
- 단일 코어 CPU에는 병렬성이 구현될 수 없다.
- 병렬성은 해야할 작업이 CPU 코어 수 보다 작거나 같을 때 가장 효율적이다.
병렬성 & 동시성 조합
ThreadPoolExecutor
CPU 1 ─── [Task] ─ [Task] ─ [Task] ──── [Task] ─────────→ Thread 1
─── [Task] ─ [Task] ─ [Task] ─ [Task] ────────────→ Thread 2
CPU 2 ─── [Task] ─ [Task] ─ [Task] ──────── [Task] ─────→ Thread 3
─── [Task] ─ [Task] ─ [Task] ──── [Task] ─────────→ Thread 4
- 스레드 풀을 만들어서 태스크를 처리하는 기법.
- 병렬성으로 처리 성능을 극대화 하고 동시성으로 CPU 자원을 효율적으로 운용한다.
- 작업간 의존이 없는 경우에 적합하다.
- 고정 스레드 풀에서 처리되고 I/O 작업에서 주로 사용된다.
ForkJoinPool
CPU 1 ─── [Task] ─→ [Task] ─┬───────────────────────────────→ Thread 1
│
CPU 2 ──────────────────────┼─→ [Task] ─────────────────────→ Thread 2
│
CPU 3 ──────────────────────┴─→ [Task] ─┼─→ [Task] ─────────→ Thread 3
│
CPU 4 ──────────────────────────────────┴─→ [Task] ─────────→ Thread 4
- 하나의 태스크를 서브 태스크로 분할하여 병렬 처리한다.
- 분할 정복 알고리즘에 최적화 되어있다.
- Stream API의 parallel()에서 사용됨. (현재 사용 가능한 코어수에 의존)
- 재귀적 작업에 적합하다.
- Virtual Thread의 처리 방식이 ForkJoinPool 방식이다.
커널 모드에 대해 알아보자. 사용자 모드 / 커널 모드
### Application Layer
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Application │ │ Application │ │ Application │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
└────────────────┼────────────────┘
│
↓
┌───────────────────────────────────────────────────┐
│ Operating System │
│ ┌─────────────────────────────────────────────┐ │
│ │ Kernel │ │
│ └─────────────────────────────────────────────┘ │
└───────────────────────┬───────────────────────────┘
│
↓
┌───────────────────────────────────────────────────┐
│ Hardware │
│ (cpu, memory, I/O 장치) │
└───────────────────────────────────────────────────┘
- 운영체제는 컴퓨터 자원을 효율적으로 관리하는 소프트웨어이고, 이를 위해 여러 기능을 갖고있다.
이중 핵심 기능을 담당하는 부분이 커널(Kernel)이다. - 운영체제 위에서 동작하는 사용자 프로그램들을 편하고 효율적으로 사용할 수 있게
하드웨어와 소프트웨어(애플리케이션) 간 중재자 역할을 하게된다. - 결국 사용자 애플레케이션은 자원을 독점하려고 하기 때문에 이를 중재하기 위한 강력한 중재자인
운영체지가 존재하고 이를 위한 주요 기능으로 커널이 존재하는 것. - 사용자 모드에서 I/O 영역을 사용하기 위해 커널 모드에 들어가야 하고 이를 위해서는 시스템 콜을 통해서만
커널 모드에 진입할 수 있다. 커널모드에 진입하면 그때 하드웨어 장치에 직접 제어가 가능해진다.
커널 수준 스레드와 사용자 수준 스레드를 알아보자.
사용자 수준 스레드
- 사용자 애플리케이션에서 관리하는 스레드이다.
- 사용자 영역으로 스레드 라이브러리들에 의해 스레드의 생성, 종료, 메시지 전달, 스케쥴링 보관 등 모든것을 관리한다.
- 커널은 사용자 수준 스레드에 대해 알지 못하고 단일 스레드 프로세스로 인식해서 관리한다.
커널 수준 스레드
- OS에서 관리하는 스레드이다.
- 커널이 스레드와 관련된 모든 작업을 관리한다. PCB, TCB 관리 등
- CPU는 커널에 의해 생성된 스레드의 실행만을 담당한다.
스레드 매핑 모델
- CPU는 OS Sccheduler가 예약하는 커널 스레드만 할당받아서 실행시키므로 사용자 수준 스레드와 커널 수준 스레드간 매핑은 필수적이다.
- 3가지 모델이 있다. 다대일 매핑 / 일대일 매핑 / 다대다 매핑
다대일 매핑
Many 사용자 수준 스레드 to One 커널 수준 스레드
- 다수의 사용자 수준 스레드가 커널 수준 스레드 하나에 매핑된다.
- 커널의 개입 없이 사용자 수준 스레드 끼리 스위칭이 발생하므로 오버헤드가 적다.
- 개별 스레드 단위가 아닌 단일 스레드의 프로세스 단위로 프로세서를 할당하므로 멀티코어를 이용한 병렬처리를 할 수 없다.
- 한 스레드가 Block I/O가 발생하면 모든 스레드들이 Block된다. 프로세스 자체를 블록하기 때문.
1번 프로세스 스레드 O, 스레드 O, 스레드 O <-> 커널 스레드 O <-> CPU Core
- 프로세스 내부에서 우선순위를 통해 커널 스레드를 점유하는 것.
일대일 매핑
One 사용자 수준 스레드 to One 커널 수준 스레드
- 사용자 수준 스레드 하나 당 커널 수준 스레드 하나가 매핑된다.
- 커널이 전체 프로세스와 스레드 정보를 유지해야하므로 컨텍스트 스위칭 시 커널 모드로 전환해서 스케쥴링이 필요하다.
- 자원의 한정으로 무한정 스레드를 만들순 없다.
- 스레드 단위로 CPU를 점유하므로 멀티코어를 활용한 병렬 처리가 가능하다.
- 스레드 하나가 IO Blocked 되더라도 다른 스레드를 사용할 수 있다. 따라서 동시성 처리가 가능하다.
- 자바의 Native Thread가 여기에 해당한다. Platform Thread라고 함.
1번 프로세스 스레드 O, 스레드 O, 스레드 O <-> 커널 스레드 O, 스레드 O, 스레드 O <-> CPU Core, Core, Core
다대다 매핑
Many 사용자 수준 스레드 to Many 커널 수준 스레드
- 여러개의 사용자 수준 스레드를 같거나 더 적은 수의 커널 수준 스레드에 매핑한다.
- 사용자는 필요한 만큼 (커널 수준 스레드 한계에 구애받지 않고) 사용자 수준 스레드를 생성할 수 있고, 커널 수준 스레드가 멀티 프로세서에서 병렬로 수행될 수 있다.
- 병렬성, 동시성 처리가 가능하다.
- Java에서는 Virtual Thread, Kotlin에서는 Coroutine 으로 다대다 스레드 매핑 모델을 구현하고 있다.
1번 프로세스 스레드 O, 스레드 O, 스레드 O, 스레드 O, ... <-> 커널 스레드 O, 스레드 O, 스레드 O <-> CPU Core, Core, Core
병렬 수행 코드 예시
코어 수와 동일한 스레드 병렬 수행
fun `코어개수와 동일한 병렬수행`() {
val core = Runtime.getRuntime().availableProcessors()
val data = ArrayList<Int>()
for (i in 1..core) {
data.add(i)
}
val start: Long = System.currentTimeMillis()
val sum = data
.parallelStream()
.mapToLong {
try {
Thread.sleep(500)
} catch (_: Exception) {
throw RuntimeException()
}
(it * it).toLong()
}.sum()
val end: Long = System.currentTimeMillis()
println("코어개수와 동일한 병렬수행: sum: $sum, duration: ${end - start}ms")
}

- sleep 시간 500ms와 유사한 시간이 소요되었다.
- 스레드수와 코어수가 동일하므로 플랫폼 스레드가 생성되어서 각각 코어에 선점되어 처리가 된 것이다.
코어 수 보다 2배 많은 스레드 병렬 수행
fun `코어개수 두배의 병렬 수행`() {
val core = Runtime.getRuntime().availableProcessors()
val data = ArrayList<Int>()
for (i in 1..core * 2) {
data.add(i)
}
val start: Long = System.currentTimeMillis()
val sum = data
.parallelStream()
.mapToLong {
try {
Thread.sleep(500)
} catch (_: Exception) {
throw RuntimeException()
}
(it * it).toLong()
}.sum()
val end: Long = System.currentTimeMillis()
println("코어개수 2배의 병렬수행: sum: $sum, duration: ${end - start}ms")
}

- sleep 시간 500ms의 2배인 1,000ms와 유사한 시간이 소요되었다.
- 스레드 수가 코어 수의 2배이므로 한차례 선점 후 sleep(500)을 하고, 다시 다음 스레드들이 선점 후 sleep을 하게된 것이다.
코어 수 보다 1개 많은 스레드 병렬 수행
fun `코어개수 보다 한 개 많은 병렬 수행`() {
val core = Runtime.getRuntime().availableProcessors()
val data = ArrayList<Int>()
for (i in 1..core + 1) {
data.add(i)
}
val start: Long = System.currentTimeMillis()
val sum = data
.parallelStream()
.mapToLong {
try {
Thread.sleep(500)
} catch (_: Exception) {
throw RuntimeException()
}
(it * it).toLong()
}.sum()
val end: Long = System.currentTimeMillis()
println("코어개수 보다 한 개 많은 병렬 수행: sum: $sum, duration: ${end - start}ms")
}

- sleep 시간 500ms의 2배인 1,000ms와 유사한 시간이 소요되었다.
- 스레드 수가 코어 수보다 1개 더 많으므로 전체 스레드 - 1개가 각 코어에 전부 선점되었고, 이후 남은 1개 스레드가 코어에 선점되어 처리가 된것이다.
이처럼 코어 수에 맞게 스레드를 병렬 수행하는것이 가장 효율적이고, 코어가 N개일 때 N개의 작업을 각 스레드에 할당하여 병렬 처리하는 소요 시간은, 1개의 작업을 단독으로 처리할 때의 시간과 거의 동일함을 확인할 수 있다.
반응형
'기술 학습 > JVM 스레드 딥다이브' 카테고리의 다른 글
| JVM 동시성 프로그래밍 딥다이브 2. 스레드 생명주기와 상태 (0) | 2025.12.21 |
|---|
Comments
반응형
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday