| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | ||
| 6 | 7 | 8 | 9 | 10 | 11 | 12 |
| 13 | 14 | 15 | 16 | 17 | 18 | 19 |
| 20 | 21 | 22 | 23 | 24 | 25 | 26 |
| 27 | 28 | 29 | 30 |
- assertJ
- 벌크연산
- RequestMappingHandlerMapping
- TDZ
- Transaction
- cross-cutting concerns
- tracking-modes
- ExceptionResolver
- 역정규화
- IllegalStateException
- API
- COPYOFRANGE
- #@Transacional
- 단어변환
- Java
- generic type
- HandlerMethod
- CORS
- type eraser
- pessimistic lock
- hoisting
- demand paging
- CQS
- 프로그래머스
- SPOF
- wrapper class
- propagation
- NestJS 요청흐름
- Generic method
- optimistic lock
- Today
- Total
jingyulog
Multi-Thread 본문
개요
멀티쓰레딩은 여러 프로세스가 동시에 실행되는 멀티 태스킹과 달리 하나의 프로세스 내에서 여러 작업을 여러 쓰레드를 통해서 동시에 실행할 수 있도록 하는 방식이다.
주요 특징
- 경량화된 실행 단위
- 낮은 오버헤드: 스레드는 같은 프로세스 내에서 실행되므로, 프로세스 간의 컨텍스트 스위칭에 비해 스레드 간 전환은 훨씬 가볍고 빠르다.
- 빠른 전환: 각 스레드는 자신만의 스택과 레지스터(PC, Program Counter)를 갖지만, 코드나 힙 메모리 등은 공유하기 때문에 전환시 재설정해야 할 데이터의 양이 적어 전환 속도가 빠르다.
- 효율적인 데이터 공유
- 공유 메모리: 같은 프로세스 내의 스레드들은 힙 영역 등 주요 메모리 공간을 공유하므로 데이터 전달이 빠르고 간편하다.
- 동기화 관리: 스레드 간의 데이터 공유는 IPC(Inter Process Communication)와 같은 복잡한 매커니즘 없이도 이루어지지만, 동시에 접근할 경우 동기화 문제는 여전히 존재한다.
- 응답성 및 처리 성능 향상
- 병렬 처리: 멀티쓰레딩을 통해 I/O 작업과 CPU 집약적 작업을 분리하여 동시에 처리할 수 있으므로 시스템 전체의 응답성이 향상된다.
- 리소스 활용 최적화: CPU의 멀티코어 환경에서 각 스레드를 개별 코어에 할당하여 병렬 처리가 가능해지므로 시스템 자원을 더욱 효유적으로 사용할 수 있다.
정리
장점
멀티쓰레딩을 사용하면 프로세스 기반의 멀티쓰레딩보다 낮은 비용의 컨텍스트 스위칭과 효율적인 메모리 사용, 그리고 빠른 데이터 공유가 가능해진다.
기대 효과
그에 따른 결과로 I/O 작업이나 대기 작업을 별도의 스레드로 처리하여 주 스레드가 차단되지 않고 사용자의 입력이나 다른 중요한 작업에 빠르게 대응할 수 있게 된다.
그럼에도 남은 문제
하지만 멀티쓰레딩도 완벽한 해결책은 아니다. 왜냐하면, 스레드들이 같은 메모리를 공유하다 보니 경쟁 상태나 교착 상태 같은 동기화 문제가 발생할 수 있고, 이를 해결하는 과정이 복잡해지거나 디버깅이 어려워질 수 있다. 또한 스레드 관리를 소홀히 하면 시스템 자원이 과도하게 사용될 위험도 있다.
경쟁 상태 (Race Condition)
여러 스레드가 동시에 공유 자원(메모리, 파일 등)에 접근 및 수정할 때 발생하는 문제로, 실행 순서나 타이밍에 따라 결과가 달라져 예상치 못한 버그가 발생한다는 문제가 있다.
예시
이 예제에서 두 개의 스레드가 동시에 increment() 실행시, count의 기댓값은 2이지만, 실제는 1이 되는 상황이 발생할 수 있다.
class Counter {
private int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}해결 방법
- 락(Lock, Mutex) 사용: 동시에 한 스레드만 사용이 가능하도록 한다.
synchronized void increment() { count++; }- 원자적 연산(Atomic Operation): 락보다 가볍고 성능이 좋다.
private AtomicInteger count = new AtomicInteger(0); public void increment() { count.incrementAndGet(); }- 불변 객체(Immutable Object) 활용
공유 자원을 수정하지 않고, 새 객체를 생성해 교체한다. (String, LocalDateTime 등)
교착 상태(Deadlock)
두 개 이상의 스레드가 서로 가진 자원을 기다리면서 무한 대기 상태에 빠지는 문제이다.
예시
- t1은 a 락을 잡고, b를 기다린다.
- t2는 b 락을 잡고, a를 기다린다.
class A {}
class B {}
A a = new A();
B b = new B();
Thread t1 = new Thread(() -> {
synchronized(a) {
synchronized(b) {
System.out.println("Thread 1 Done");
}
}
})
Thread t2 = new Thread(() -> {
synchronized(b) {
synchronized(a) {
System.out.println("Thread 2 Done");
}
}
})해결 방법
- 모든 스레드의 락 획득 순서를 통일한다.
- 타임아웃 기반 락을 활용한다. (Java: tryLock(long timeout, TimeUnit unit))
- 교착 상태 회피 알고리즘
은행원 알고리즘 등을 사용해 자원 할당시 교착 가능성을 체크한다. (OS level) - 공유 자원 자체를 줄이고, 락을 가능한 한 짧게 유지한다.
Lock
공유 자원에 대한 배타적 접근을 보장하느 기법을 아우르는 용어로 구현 방식에는 Mutex, ReadWriteLock, SpinnLock 등이 있다. Java 에서는 synchronized, ReentrantLock이 Lock의 구현체이다.
Mutex(Mutual Exclusion)
- 락의 구체적인 구현체 중 하나로 항상 한 순간에 단 하나의 스레드만 접근 가능하도록 보장한다.
- 이진(Binary) 상태(열려 있음(0) 또는 닫혀 있음(1))로 존재하며, 오직 1개의 스레드만 임계 구역(critical section)에 들어갈 수 있다.
ReadWriteLock
- 읽기 작업은 동시에 여러 스레드가 가능하지만, 쓰기 작업은 한 번에 하나만 허용하는 락으로, 읽기-쓰기 성능을 최적화할 때 사용한다.
- 다수의 Reader는 동시에 접근이 가능하다.
- Writer는 Reader/Writer가 아무도 없을 때만 접근이 가능하다.
- Java의 ReentrantReadWriteLock을 사용한다.
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock readLock = rwLock.readLock();
Lock writeLock = rwLock.writeLock();
public void readDate() {
readLock.lock();
try {
// 여러 스레드가 동시에 읽을 수 있다.
} finally {
readLock.unlock();
}
}
public void writeDate() {
writeLock.lock();
try {
// 쓰기는 오직 한 스레드만 가능하다.
} finally {
writeLock.unlock();
}
}SpinLock
- 락을 얻을 때, 무한히 반복해서 lock이 풀릴 때까지 확인하는 방식이다.
- Context Switching 비용이 없으므로 락이 짧게 유지될 경우 성능에 유리하다. 하지만 락이 오래 걸리면 CPU를 계속 낭비한다.
- Java에서는 직접 구현하거나, AtomicBoolean 같은 Compare And Set 연산으로 만들 수 있다.
class SpinLock { private final AtomicBoolean locked = new AtomicBoolean(false); public void lock() { while (!locked.compareAndSet(false, true)) { // busy-wait } } public void unlock() { locked.set(false); } }
synchronized, ReentrantLock: Java에서 제공하는 Lock
synchronized (무조건 블로킹)
- Java에서 제공하는 가장 기본적인 락으로 해당 키워드의 블록/메서드에 들어가면, 해당 객체의 모니터 락(Monitor Lock)을 획득할 수 있으며, 한 번에 하나의 스레드만 임계 구역 실행이 가능하다.
- JVM 레벨에서 지원하며, 세밀한 제어(tryLock, timeout 등)이 불가능하다.
- 오래 기다리면 블로킹한다. 즉, 해당 스레드는 자원을 기다리면서 CPU를 점유하지 않고, 대기열(wait queue)에 들어가서 운영체제가 깨워줄 때까지 멈춘다. 따라서 락을 오래 쥐고있는 스레드가 있으면, 대기 중인 스레드들은 계속 블로킹 상태가 된다.
ReentrantLock (블로킹/비블로킹 선택 가능)
- Reentrant : 같은 스레드가 여러 번 락을 잡을 수 있음을 의미한다. 즉, 재진입이 가능하다.
- synchronized보다 유연하게 제어가 가능한 락이다.
- lock(), unlock() 을 명시적으로 호출해야 한다.
- tryLock() : 락을 얻지 못하면 즉시 리턴한다. -> 블로킹이 안된다.
- tryLock(timeout) : 일정 시간 동안만 시도한다.
- lockInterruptibly() : 다른 스레드가 인터럽트로 락 대기 중단이 가능하다.
- 공정성(Fairness) 설정이 가능하다.
ReentrantLock lock = new ReentrantLock();
if (lock.tryLock()) {
try {
// 락 획득 성공 → 작업 진행
} finally {
lock.unlock();
}
} else {
// 락 못 얻으면 그냥 다른 로직 처리 가능
}Semaphore
- 동시에 접근할 수 있는 스레드의 개수를 제한하는 동기화 도구이다.
- 내부적으로 카운터 값을 가진다.
- acquire(): 카운터 감소 (자원 획득)
- release(): 카운터 증가 (자원 반납)
- 즉, 카운터가 1인 세마포어는 사실상 뮤텍스와 동일하다.
- 카운터가 N이면, 동시에 최대 N개의 스레드가 접근이 가능한 것이다.
- 따라서 자원 개수가 여러 개인 경우(Thread Pool, DB Connection Pool)에 유용하다.
'컴퓨터 사이언스 > 컴퓨터 구조' 카테고리의 다른 글
| 페이지 교체 알고리즘 (0) | 2025.10.04 |
|---|---|
| javascript, java, kotlin에서 비동기를 다룰 때 어떤 차이가 있을까? (0) | 2025.10.04 |
| 명령어 사이클 (0) | 2023.11.15 |
| 레지스터의 역할과 종류 (0) | 2023.11.15 |
| Segmentation과 Paging의 개념 및 장단점 정리 (0) | 2023.11.14 |