프로세스(process) : 실행 중인 프로그램
실행에 필요한 데이터와 메모리 등의 자원, 쓰레드로 구성되어 있다
쓰레드(Thread) : 프로세스 내에서 실행되는 흐름의 단위. 경량 프로세스(Light-Weight Process)라고 부르기도 한다
프로세스라는 공장에서 작업을 처리하는 일꾼으로 생각하면 쉽다
프로세스가 가질 수 있는 쓰레드 개수는 제한되어 있지 않지만 쓰레드가 작업을 수행하는데 개별적인 메모리 공간(콜스택)을 필요로 하기 때문에 프로세스의 메모리 한계에 따라 쓰레드의 수가 결정된다
멀티태스킹(Multi Tasking) : 여러 개의 프로세스를 동시에 실행하는 것
멀티쓰레딩(Multi Threading) : 여러 개의 쓰레드가 동시에 작업을 수행하는 것
CPU의 코어는 한 번에 하나의 작업만 수행할 수 있기 때문에 단순히 쓰레드의 수가 많다고 성능이 좋은 프로세스는 아니다
* 실행 중인 사용자 쓰레드가 하나도 없을 때 프로그램은 종료된다
class MyThread extends Thread {
@Override
public void run() {...} // Thread의 run()을 오버라이딩
}
// main
MyThread t = new MyThread();class MyThread implements Runnable {
@Override
public void run() {...}
}
// main
Thread t = new Thread(new MyThread());| 메서드 | 설명 |
|---|---|
start() | 새로운 쓰레드를 시작하고, 내부적으로 run() 메서드를 호출함 |
run() | 쓰레드가 실행할 작업을 정의함 (Runnable을 구현하거나 오버라이딩) |
sleep(long millis) | 현재 실행 중인 쓰레드를 지정한 시간(ms) 동안 멈춤 |
join() | 다른 쓰레드가 종료될 때까지 현재 쓰레드를 대기시킴 |
join(long millis) | 지정한 시간 동안만 기다림 |
interrupt() | 쓰레드에 인터럽트 요청을 보냄 (중단 요청) |
isInterrupted() | 쓰레드가 인터럽트 되었는지 여부를 확인함 |
interrupted() | 현재 쓰레드의 인터럽트 상태를 확인하고 초기화함 (static 메서드) |
setPriority(int newPriority) | 쓰레드의 우선순위를 설정함 (1~10) |
getPriority() | 현재 쓰레드의 우선순위를 반환함 |
setName(String name) | 쓰레드 이름을 설정함 |
getName() | 쓰레드의 이름을 반환함 |
isAlive() | 쓰레드가 아직 실행 중인지 여부를 반환함 |
currentThread() | 현재 실행 중인 쓰레드 객체를 반환함 (static 메서드) |
yield() | 현재 실행 중인 쓰레드가 다른 쓰레드에게 실행 양보 (static 메서드) |
setDaemon(boolean on) | 데몬 쓰레드로 설정 (true: 백그라운드 쓰레드로 동작) |
isDaemon() | 쓰레드가 데몬인지 확인 |
싱글 코어에서 단순히 CPU만을 사용하는 계산작업은 멀티쓰레드보다 싱글쓰레드가 효율적이다
왜?
멀티쓰레드는 컨텍스트 스위칭으로 인한 오버헤드가 발생하기 때문이다
또한, I/O 작업이 있는 경우 필요한 자원을 공유하게 되어 한 쪽 쓰레드는 대기하는 시간이 발생한다
반대로 I/O 작업이 하나의 쓰레드에만 있는 경우에는 I/O 작업동안 다른 쓰레드가 실행될 수 있으므로 멀티쓰레드가 더 유리하다
void setPriority(int newPriority) : 쓰레드의 우선순위를 지정한 값으로 변경한다
int getPriority() : 쓰레드의 우선순위를 반환한다
최소 1에서 최대 10이다
| 상수 | 값 | 설명 |
|---|---|---|
Thread.MIN_PRIORITY | 1 | 가장 낮은 우선순위 |
Thread.NORM_PRIORITY | 5 | 기본 우선순위 |
Thread.MAX_PRIORITY | 10 | 가장 높은 우선순위 |
우선순위를 지정한다고 해서 반드시 높은 우선순위가 빨리 완료되는 것은 아니다
왜?
OS의 스케쥴링 정책과 JVM의 구현에 따라 쓰레드가 실행되기 때문이다
서로 관련된 쓰레드를 그룹으로 다루기 위해 사용된다
| 생성자 | 설명 |
|---|---|
ThreadGroup(String name) | 지정된 이름을 가진 새 ThreadGroup을 생성합니다. |
ThreadGroup(ThreadGroup parent, String name) | 지정된 상위 ThreadGroup과 이름을 가진 새 ThreadGroup을 생성합니다. |
| 메서드 | 설명 |
|---|---|
int activeCount() | 이 ThreadGroup에 속하거나 하위 그룹에 속한 활성 스레드의 수를 반환합니다. |
int activeGroupCount() | 이 ThreadGroup의 하위 그룹 수를 반환합니다. |
void checkAccess() | 현재 스레드가 이 ThreadGroup에 대한 접근 권한을 가지고 있는지 확인합니다. |
void destroy() | 이 ThreadGroup과 하위 그룹을 제거합니다. (더 이상 사용할 수 없음) |
int enumerate(Thread[] list) | 이 ThreadGroup에 속한 활성 스레드를 배열에 복사합니다. |
int enumerate(ThreadGroup[] list) | 이 ThreadGroup의 하위 그룹을 배열에 복사합니다. |
String getName() | ThreadGroup의 이름을 반환합니다. |
ThreadGroup getParent() | 상위 ThreadGroup을 반환합니다. |
int getMaxPriority() | 이 ThreadGroup의 최대 우선순위를 반환합니다. |
boolean isDaemon() | 이 ThreadGroup이 데몬 그룹인지 여부를 반환합니다. |
boolean isDestroyed() | 이 ThreadGroup이 이미 destroy() 호출로 파괴되었는지 확인합니다. |
void setDaemon(boolean daemon) | 이 ThreadGroup을 데몬 그룹으로 설정합니다. |
void setMaxPriority(int pri) | 이 ThreadGroup의 최대 우선순위를 설정합니다. |
String toString() | 이 ThreadGroup에 대한 문자열 표현을 반환합니다. |
Thread 클래스의 생성자를 사용해서 해당 쓰레드를 그룹에 포함시킬 수 있다
모든 쓰레드는 반드시 쓰레드그룹에 포함되어 있어야 한다
→ 생성하는 모든 쓰레드그룹은 main 쓰레드 그룹의 하위 쓰레드 그룹이 되며, 쓰레드 그룹을 지정하지 않으면 자동으로 main 쓰레드 그룹에 속한다
| 생성자 | 설명 |
|---|---|
Thread(ThreadGroup group, Runnable target) | 지정된 ThreadGroup과 Runnable 객체로 새 스레드를 생성합니다. |
Thread(ThreadGroup group, Runnable target, String name) | 지정된 ThreadGroup, Runnable, 스레드 이름으로 새 스레드를 생성합니다. |
Thread(ThreadGroup group, Runnable target, String name, long stackSize) | 지정된 ThreadGroup, Runnable, 이름, 스택 크기로 스레드를 생성합니다. |
Thread(ThreadGroup group, String name) | 지정된 ThreadGroup과 이름으로 새 스레드를 생성합니다. |
| 메서드 | 설명 |
|---|---|
ThreadGroup getThreadGroup() | 현재 스레드가 속한 ThreadGroup을 반환합니다. |
void uncaughtException(Thread t, Throwable e) | 처리되지 않은 예외가 발생했을 때 호출되며, 예외가 발생한 스레드와 예외 객체를 인자로 받습니다. (UncaughtExceptionHandler 인터페이스에 정의됨) |
데몬 쓰레드(Daemon Thread) : 일반 쓰레드의 작업을 돕는 보조 쓰레드
무한 루프와 조건문으로 구성되어 대기하고 있다가 특정 조건이 만족되면 작업을 수행한다
e.g. 가비지컬렉터, 워드프로세서 자동저장, 화면자동갱신, ...
void setDaemon(boolean on) 메서드로 데몬 쓰레드로 변경할 수 있다. 이 메서드는 반드시 start() 메서드를 호출하기 전에 실행되어야 한다
데몬 쓰레드가 생성한 쓰레드는 자동적으로 데몬 쓰레드가 된다
| 상태 | 설명 |
|---|---|
NEW | 스레드 객체가 생성되었지만 아직 start()가 호출되지 않은 상태입니다. |
RUNNABLE | 스레드가 실행 중이거나 실행을 기다리는 상태입니다 (JVM이 실행할 준비 완료). |
BLOCKED | 다른 스레드가 모니터 락을 가지고 있어서, 해당 락을 얻기 위해 대기 중인 상태입니다. |
WAITING | 다른 스레드가 명시적으로 notify() 또는 interrupt() 하기 전까지 무한히 기다리는 상태입니다. |
TIMED_WAITING | 일정 시간 동안만 기다리는 상태 (sleep(), join(timeout), wait(timeout) 등). |
TERMINATED | 스레드의 실행이 종료된 상태입니다. (run() 메서드가 끝난 경우 등) |
출처 : velog
| 메서드 | 설명 |
|---|---|
static void yield() | 현재 실행 중인 스레드가 CPU를 다른 스레드에게 양보하도록 요청합니다. |
static void sleep(long millis) | 지정된 시간(ms) 동안 현재 스레드를 일시 정지시킵니다. |
static void sleep(long millis, int nanos) | 밀리초 + 나노초 단위로 일시 정지합니다. |
void join() | 다른 스레드가 종료될 때까지 현재 스레드를 기다립니다. |
void join(long millis) | 최대 millis 시간 동안 기다립니다. |
void join(long millis, int nanos) | 최대 millis + nanos 시간 동안 기다립니다. |
지정된 시간이 다 되거나 interrupt() 가 호출되면 실행대기 상태가 된다
→ interrupt() 가 InterruptedException 을 발생시키므로 sleep() 은 항상 try-catch 문으로 예외 처리를 해줘야 한다 (정확히는 interrupt() 가 호출되면 JVM이 현재 쓰레드를 깨우고 InterruptedException 을 던진다)
항상 현재 실행 중인 쓰레드에 대해 작동한다
→ 참조변수를 이용해서 호출해도 해당 변수와 관계없이 현재 쓰레드에 대해 작동하므로 Thread.sleep(2000) 의 형태로 호출해야 한다
try {
th1.sleep(2000); // th1 쓰레드를 일시정지하는 게 아니라 현재 쓰레드가 일시정지된다
} catch (InterruptedException e) {}
busy-waiting(의미없이 실행시간을 소비하는 경우) 상황일 때, 남은 실행시간을 다른 쓰레드에게 양보한다
| 메서드 | 설명 |
|---|---|
void interrupt() | 해당 스레드에 인터럽트를 발생시킵니다. (주로 sleep, wait 중인 스레드를 깰 때 사용) |
boolean isInterrupted() | 해당 스레드에 인터럽트 요청이 들어왔는지 확인합니다. |
static boolean interrupted() | 현재 스레드의 인터럽트 상태를 확인하고, 상태를 초기화합니다.(interrupted 변수를 false로 초기화) |
interrupt() 는 작업을 멈추라고 요청할 뿐 강제로 종료시키지는 못한다 (interrupted 변수를 true로 바꾼다)
public void suspend() {
suspended = true;
th.interrupt();
}
public void stop() {
stopped = true; // stopped 라는 플래그만 변경한다면 확인을 위해 항상 1초는 기다려야 한다
th.interrupt(); // interrupt 까지 호출하면 기다릴 필요없이 바로 예외가 발생한다
}
while (!stopped) {
if (!suspended) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {}
} else { // suspended가 true이고 이 else 문이 없었다면 CPU 시간을 의미없이 소비하게 된다
Thread.yield(); // 의미없이 소비하지 않고 다른 쓰레드에게 양보한다
}
}
아래 메서드들은 안정성 문제(dead-lock 가능성이 높음)로 더 이상 사용이 권장되지 않으며 InterruptedException, volatile, Lock, Condition 등을 사용하는 방식으로 대체되어야 한다
| 메서드 | 설명 | 이유 |
|---|---|---|
void stop() | 스레드를 즉시 종료시킵니다. (Deprecated) | 스레드를 중단시키되 lock을 유지함 → 다른 스레드가 lock 잡을 수 없음 |
void suspend() | 스레드를 일시 정지시킵니다. (Deprecated) | suspend()된 스레드를 다시 실행시키지만, 호출 기회를 못 가지면 deadlock |
void resume() | suspend()된 스레드를 다시 실행시킵니다. (Deprecated) | 스레드를 즉시 죽이지만, lock을 해제하지 않음 → 공유 자원 사용 불가 상태 |
한 쓰레드가 진행 중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는 것
임계 영역(critical section) : 둘 이상의 쓰레드가 동시에 접근해서는 안되는 공유 자원에 접근하는 코드
임계 영역을 지정할 수 있다
public synchronized void method() { ... }
syncrhonized(락을 걸고자 하는 객체의 참조변수) { ... }
1번 보다는 2번으로 임계 영역을 최소화하여 프로그램을 효율성을 높여야 한다
synchronized 블럭을 벗어나거나 블럭 내에서 에러가 발생하면 락이 풀린다.
또한, synchronized 블럭을 들어갈 때와 나올 때 캐시와 메모리 간의 동기화가 일어난다
wait() : 임계 영역의 코드를 수행하다가 작업을 더 이상 진행할 상황이 아닐 때 쓰레드가 락을 반납하고 기다리게 한다
notify() : 작업을 진행할 수 있게 되면 notify() 를 호출해서 기다리던 쓰레드가 다시 락을 얻고 작업을 진행한다
waiting pool : 쓰레드가 wait()로 인해 일시 정지되면 들어가는 장소
notifyAll() 로 대기 중인 모든 쓰레드를 깨우면 하나의 쓰레드만 락을 얻고 작업을 수행한다starvation : 쓰레드의 우선순위가 낮거나 단순한 우연으로 순위가 계속 밀려 작업을 수행하지 못하는 상황
race condition : 공유 자원에 대해 락을 얻기 위해 서로 경쟁하는 상태
→ 기능이 다른 쓰레드(Read 쓰레드, Write 쓰레드)를 구별해서 통지하기
→ ReentrantLock과 Condition 클래스 사용
예시
| 시점 | Thread A | Thread B | 공유 변수 (counter) |
|---|---|---|---|
| T1 | load → 5 | 5 | |
| T2 | load → 5 | 5 | |
| T3 | add → 6 | ||
| T4 | add → 6 | ||
| T5 | store → 6 | 6 (A가 저장) | |
| T6 | store → 6 | 6 (B가 덮어씀) |
| 항목 | ReentrantLock | ReentrantReadWriteLock | StampedLock |
|---|---|---|---|
| 제공 클래스 | java.util.concurrent.locks.ReentrantLock | java.util.concurrent.locks.ReentrantReadWriteLock | java.util.concurrent.locks.StampedLock |
| 읽기/쓰기 락 분리 | ❌ 단일 락 | ✅ readLock() / writeLock() | ✅ readLock() / writeLock() / optimisticRead() |
| 재진입 가능 여부 | ✅ 가능 | ✅ 읽기/쓰기 모두 가능 | ❌ 비재진입 (예외 발생 가능) |
| 공정성 옵션 | ✅ 생성자에서 설정 가능 (true) | ✅ 읽기/쓰기 모두 가능 (true) | ❌ 없음 |
| tryLock 지원 | ✅ 있음 | ✅ 있음 (readLock().tryLock(), 등) | ✅ 있음 (타임아웃 포함) |
| 락 해제 방식 | lock.unlock() | readLock().unlock(), writeLock().unlock() | unlockRead(), unlockWrite(), unlock(stamp) |
| 낙관적 읽기 (Optimistic Read) | ❌ 없음 | ❌ 없음 | ✅ 매우 빠름 (검증 필요) |
| 스레드 간 안전 | ✅ | ✅ | ✅ (주의해서 사용 필요) |
| 사용 난이도 | ⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ (조금 더 복잡함) |
| 대표 용도 | 일반적인 상호 배제 락 | 읽기 많은 환경에서 성능 향상 | 읽기 위주 환경 + 높은 성능 요구 시 |
* 낙관적 읽기(Optimistic Read) : 데이터를 읽을 때 락을 먼저 걸지 않고 읽은 후에 다른 쓰레드가 쓰기를 했는지 검사해서 문제가 없으면 그대로 사용, 문제가 있으면 재시도 또는 정식으로 락을 걸어서 읽는 방식
DB 트랜잭션 격리 수준의 READ COMMITTED 와 유사
| 생성자 | 설명 |
|---|---|
ReentrantLock() | 기본 생성자. 비공정(FIFO 아님) 락 생성 |
ReentrantLock(boolean fair) | true이면 공정성(fairness) 보장 (FIFO 락) |
| 메서드 | 설명 |
|---|---|
lock() | 락을 획득할 때까지 현재 스레드 대기 (interruption 불가) |
lockInterruptibly() | 락을 기다리는 중 인터럽트가 가능 |
tryLock() | 락을 시도하고 즉시 성공/실패 반환 |
tryLock(long time, TimeUnit unit) | 일정 시간 동안 락을 시도하고 성공/실패 반환 |
unlock() | 락 해제. 락을 소유한 스레드만 호출 가능 |
isLocked() | 현재 락이 걸려 있는지 여부 반환 |
isHeldByCurrentThread() | 현재 스레드가 이 락을 소유하고 있는지 여부 반환 |
getHoldCount() | 현재 스레드가 이 락을 몇 번 재진입했는지 반환 |
isFair() | 이 락이 공정 모드로 생성되었는지 반환 |
hasQueuedThreads() | 현재 대기 중인 스레드가 있는지 여부 반환 |
getQueueLength() | 대기 중인 스레드 수 반환 (추정값) |
newCondition() | Condition 객체 생성 (wait/notify 기능 지원) |
| 생성 방법 | 설명 |
|---|---|
Condition condition = lock.newCondition(); | ReentrantLock에서만 생성 가능 (생성자 없음) |
Condition 객체는 ReentrantLock이 관리하며, newCondition() 메서드로 생성됨Condition은 고유한 대기열(Waiting Queue) 을 가짐| 메서드 | 설명 |
|---|---|
await() | 현재 스레드를 condition queue에 넣고 대기. signal() 호출될 때까지 블로킹 |
await(long time, TimeUnit unit) | 일정 시간만 기다림. 시간 초과 시 자동 해제 |
awaitNanos(long nanosTimeout) | 대기 시간(나노초)을 반환하면서 대기 |
awaitUntil(Date deadline) | 특정 시간까지 대기 |
signal() | 대기 중인 스레드 중 하나를 깨움 |
signalAll() | 대기 중인 모든 스레드를 깨움 |
await(), signal(), signalAll() 모두 반드시 락을 소유한 상태에서 호출해야 함lock.lock();
try {
while (!조건) {
condition.await();
}
// 조건 만족 시 처리
} finally {
lock.unlock();
}signal()과 signalAll()은 notify(), notifyAll()과 유사하지만, 락과 완전히 분리되어 작동 가능| 항목 | Object.wait() | Condition.await() |
|---|---|---|
| 동작 대상 | synchronized 블록 | ReentrantLock에서만 사용 가능 |
| 여러 조건 지원 | ❌ 불가능 | ✅ 여러 개의 Condition 사용 가능 |
| 시간 제어 | 제한적 (wait(timeout)) | ✅ 다양한 시간 기반 메서드 제공 |
| 인터럽트 처리 | 기본 지원 | ✅ InterruptedException 명확히 처리 가능 |
volatile : Java에서 변수의 값을 모든 스레드에서 항상 메인 메모리의 최신 값으로 유지하도록 보장하는 키워드
즉, 스레드 간의 가시성(visibility) 문제를 해결해주는 수단
멀티 코어 프로세서에서는 코어마다 별도의 캐시를 가지고 있다
→ 메모리 접근은 느리고 비싸기 때문에 특정 조건에만 값을 메모리에 반영한다(쓰기 지연)
쓰기 지연(write-back, lazy write)에서 메모리에 반영되는 시점
synchronized 블럭volatile 키워드가 있는 변수를 변경할 때가시성 문제(visibility issue) 예시
JVM은 4byte 단위로 데이터를 처리하는데 long 과 double 은 8byte 이므로 읽는 과정 중에 다른 쓰레드가 끼어들 여지가 있다
→ 읽고 쓰는 모든 문장을 synchronized 블럭으로 감싼다
→ volatile 키워드를 사용한다
volatile 키워드를 사용했다고 해서 다른 메서드(getter, setter, ...)를 동기화하지 않아도 되는 것은 아니다
→ volatile 은 가시성만 보장하고 원자성이나 복합적인 연산에 대한 동기화는 보장하지 않는다