싱글 스레드 & 멀티 스레드
개요
- 프로세스는 오직 한개의 스레드로만 구성되는 싱글 스레드 프로세스와 하나 이상의 스레드로 구성되는 멀티 스레드 프로세스로 구분된다.
- 스레드의 선택 기준은 어떤 방식이 더 효율적으로 자원을 사용하고 성능처리에 유리한가에 있다.

싱글스레드 장점
- 문맥교환 비용이 없다.
- 동기화 이슈가 없다.
- 자원 비용이 적다.
멀티스레드 장점
- 동시성으로 사용자 응답성이 향상된다.
- CPU 멀티코어의 병렬성으로 성능이 향상된다.
- 한 스레드의 오류가 다른 스레드로 전파되지 않는다.
멀티스레딩과 동시성
- CPU의 동시적 작업 처리는 CPU 코어 개수보다 스레드의 개수가 많을 때, 즉 멀티스레딩 환경에서 자원을 효율적으로 배분하고 사용하기 위해 설계된 방식이다.
- 같은 프로그램 안에서 실행되는 여러 스레드가 읽기/쓰기 작업을 같은 메모리 영역에서 동시에 실행할 때 동시성 문제가 야기된다.
동시성 문제란?
하나의 스레드가 어떤 메모리 영역의 데이터를 사용중인데 다른 스레드가 같은 메모리 영역의 데이터를 읽거나 쓸 경우에 발생되는 문제이다.
동시성으로 인해 알아야할 동기화 관련 주제
o Critical Section
o CPU Process Architecture
o Thread-safety
o Mutual Exclusion
o Semaphore
o Monitor
o Race Condition
o Deadlock
o Starvation
o Volatile
o Synchronized
o wait / notify
o SpinLock / Busy Waiting
o Lock
o Condition
o CAS (Compare and Swap)
o Atomic Variables
코드예시
싱글 스레드
fun main() {
val start: Long = System.currentTimeMillis()
var sum = 0
for (i in 1..5) {
Thread.sleep(1000)
sum += i
if (i == 3) throw RuntimeException("의도적 에러 발생")
println("작업 $i 완료")
}
println("단일 스레드 처리 결과: sum = $sum")
val end: Long = System.currentTimeMillis()
println("단일 스레드 처리 시간: ${end - start}ms")
}

- 싱글 스레드인 경우 Main 스레드 홀로 프로그램이 수행된다.
- 따라서 예외가 던져지는 경우 바로 메인 스레드가 중단되어 프로그램이 중단된다.
멀티 스레드
fun main() {
val thread1 = Thread {
var sum = 0
for (i in 1..5) {
Thread.sleep(1000)
sum += i
printWithThread("작업 $i 완료")
}
}
val thread2 = Thread {
var product = 1
for (i in 1..5) {
Thread.sleep(500)
product *= i
if (i == 4) throw RuntimeException("의도적 에러 발생 in 스레드 2")
printWithThread("작업 $i 완료")
}
}
thread1.start()
thread2.start()
thread1.join()
thread2.join()
}

- 멀티스레드의 경우 복수의 플랫폼 스레드가 생성되어 작업을 수행하게 된다.
- 메인 스레드가 아닌 별도로 생성된 스레드가 중단되는 것이므로 메인 스레드는 끝까지 작업을 완료할 수 있다.
- 즉, 스레드의 오류가 다른 스레드로 전파되지 않는 것을 확인할 수 있다.
동기화와 CPU 관계
동기화란?
- 프로세스 혹은 스레드 간 공유 영역에 대한 동시접근으로 인해 발생하는 데이터 불일치를 막고 데이터 일관성을 유지하기 위해 순차적으로 공유 영역을 수행하도록 보장하는 매커니즘이다.
CPU 연산 처리 이해
- 모든 기계어 명령은 원자성을 갖는다. 하나의 기계어 명령어가 실행을 시작할 경우 그 명령의 수행 종료 시 까지는 인터럽트를 받지 않는다.
- CPU가 두 개 이상의 명령어를 처리할 경우에는 원자성이 보장되지 않는다. 이는 각 명령을 수행하는 중 OS가 다른 스케줄링으로 CPU에게 다른 명령을 수행하게 함으로서 현재 수행중인 명령을 인터럽트(중단)한다는 의미이다.
- 두 개 이상의 명령어를 원자성으로 묶기 위해서는 스레드 간 동기화 매커니즘이 필요하다. 즉, 한 스레드가 모든 명령을 다 수행될 때까지 도중에 중단되지 않도록 해야한다.
{
data = data + 1
}
--- 기계어 변환 ---
{
LOAD R1, data <---- 하나의 기계어 명령어. 원자성 보장. 동시성 문제 없음.
ADD R1, 1
STORE R1, data
} <---- 두 개 이상의 기계어 명령어. 원자성 보장 안됨. 동시성 문제 발생 가능. 동기화 필요.
동기성 문제 예시 코드
fun main() {
var count = 0
val thread1 = Thread {
for (i in 1..10000) {
count++
}
}
val thread2 = Thread {
for (i in 1..10000) {
count++
}
}
thread1.start()
thread2.start()
thread1.join()
thread2.join()
printWithThread(count)
}

- 1만번 카운팅 루프를 2개의 스레드에서 수행했으니 2만이 되어야 한다.
- 하지만 위에 기계어 변환 코드처럼 동시성 이슈가 발생되어 누락되는 카운트가 발생되는 동시성 문제가 발생되어
2만보다 적은 수가 카운팅 되었다.
Critial Section 임계영역
Critial Section이란?
- 둘 이상의 스레드가 동시에 접근해서는 안되는 공유 자원에 접근하는 코드 영역을 말한다.
- 임계 영역은 entry section, critical section, exit section, remainder section으로 구성된다.
입장 영역, 임계 영역, 퇴장 영역, 나머지 영역 예시
- 입장영역: lock.lock()
- 임계영역: count++
- 퇴장영역: lock.unlock()
- 나머지영역: log.info(count)
Critical Section Problem
- 한 스레드가 임계 영역을 실행하고 있을 때, 다른 스레드가 같은 임계 영역을 사용함으로서 발생하는 문제.
- 이 문제를 해결하기 위해서는 3가지 충족 조건이 요구된다.
- Mutual Exclusion (상호 배제): 임계 구역을 실행중이면 다른 스레드는 동일 임계 구역을 실행할 수 없음.
- Progress (진행): 임계 구역에 실행중인 스레드가 없고, 진입하려는 스레드가 있을 때 어떤 스레드가 들어갈 것인지 선택해줘야 하며 결정이 무한정 미뤄져선 안된다.
- Bounded Waiting (한정대기): 다른 스레드가 임계 구역에 들어가도록 요청하고 수락 되기 전에, 기존 스레드가 임계영역에서 실행할 수 있는 횟수에 제한이 있어야 한다. 기아상태가 발생하지 않도록 해야한다.
동기화 도구들
- 뮤텍스, 세마포어, 모니터, Compare And Swap(CAS) 와 같은 동기화 도구들을 통해 Critical Section 문제가 발생하지 않도록 할 수 있다.
- 자바에서는 synchronized 키워드 부터 여러 동기화 도구를 제공한다.
코드예시
fun main() {
val sharedResource = SharedResource()
val thread1 = Thread {
for (i in 1..100) {
sharedResource.increment()
}
}
val thread2 = Thread {
for (i in 1..100) {
sharedResource.increment()
}
}
thread1.start()
thread2.start()
thread1.join()
thread2.join()
}
class SharedResource {
private var counter = 0
fun increment() { // Entry Section
synchronized(this) {
// Critical Section
counter++
printWithThread("Counter: $counter")
} // Exit Section
doWork() // Remainder Section
}
private fun doWork() {
// Simulate some non-critical work
Thread.sleep(1)
}
}

- 임계영역에 대한 Entry, Critical, Exit, Remainder 영역에 대해 확인해 볼 수 있다.
안전한 스레드 구성
안전한 스레드란?
- 여러 스레드에서 클래스나 객체에 동시에 접근해서 계속 실행하더라도 지속적인 정확성이 보장되는 코드.
이를 "스레드 세이프(Thread Safe) 하다"고하며, 곧 "스레드에 안전하다"고 하는것이다. - 기본적으로 클래스 명서에 스레드 안정성을 헤치는 코드나 상태를 갖고 있지 않으면 스레드에 안전하다고 정의할 수 있다.
- 스레드에 안전한 코드에는 경쟁 상태가 없으며 경쟁 상태는 다수의 스레드가 공유자원에 쓰기작업을 시도할 때 발생한다.
따라서, 스레드가 실행될 때 어떤 자원을 공유하는지 아는것이 중요하다.
스레드에 안전한 구조
- 임계영역을 동기화 한다.
- 동시에 여러개의 스레드가 임계영역에 접근하지 못하도록 Lock 매커니즘을 사용한다.
- 동기화 도구를 사용한다.
- 세마포어, CAS, Atomic 변수, 동시성 자료구조 (Thread Safe 자료구조)들을 사용한다.
- 스레드의 스택에 한정해서 상태를 관리한다.
- 스레드마다 할당된 스택 메모리 내에서 상태를 관리한다. 이를 통해 다른 스레드와 상태를 공유할 수 없도록 함.
- ThreadLocal을 사용한다.
- 스레드마다 갖는 전용 저장소인 ThreadLocal을 사용해서 상태를 관리한다.
- 불변 객체를 사용한다.
- 객체의 상태를 변경할 수 없는 클래스를 사용하거나 상태를 변경할 수 없도록 클래스를 만들어서 스레드에 안전하게 한다.
스레드의 스택 한정
지역 변수
- 기본형 지역 변수는 스레드 마다 독립적으로 가지고 있는 스택에 저장되기 때문에 스레드간에 공유될 수 없다. 스레드에 안전한다.
- 메서드로 전달되는 기본형 파라미터 변수도 스택에서만 관리되므로 스레드에 안전하다
'기술 학습 > JVM 스레드 딥다이브' 카테고리의 다른 글
| JVM 동시성 프로그래밍 딥다이브 4-2. 스레드 활용 - ThreadLocal (0) | 2026.02.01 |
|---|---|
| JVM 동시성 프로그래밍 딥다이브 4-1. 스레드 활용 - 예외처리, 유저/데몬 스레드, ThreadGroup (0) | 2026.01.18 |
| JVM 동시성 프로그래밍 딥다이브 3. 스레드 기본 API - Sleep, Join, Interrupt 등 (1) | 2026.01.04 |
| JVM 동시성 프로그래밍 딥다이브 2. 스레드 생명주기와 상태 (0) | 2025.12.21 |
| JVM 동시성 프로그래밍 딥다이브 1. 프로세스 & 스레드 (1) | 2025.12.07 |