동시성
여러 작업이 동시에 실행되는 것처럼 보이도록 처리하는 개념입니다.
동시성 vs 병렬성
동시성 (Concurrency)
→ 하나의 코어가 여러 작업을 번갈아 처리
→ 동시에 실행되는 것처럼 보이지만 실제론 교차 실행
CPU: [작업A] [작업B] [작업A] [작업B]
병렬성 (Parallelism)
→ 여러 코어가 여러 작업을 진짜 동시에 처리
CPU1: [작업A] [작업A]
CPU2: [작업B] [작업B]
동시성 문제
1. 경쟁 조건 (Race Condition)
// 두 스레드가 동시에 count++ 실행 시
class Counter {
private int count = 0;
public void increment() {
count++; // X 원자적 연산이 아님
// 내부적으로: read → modify → write 3단계
// 스레드 간 끼어들기 가능 → 결과 예측 불가
}
}
// 예상: 1000, 실제: 997 (스레드 간 덮어쓰기 발생)
2. 교착 상태 (Deadlock)
// 스레드 A: lock1 잡고 lock2 기다림
// 스레드 B: lock2 잡고 lock1 기다림
// → 서로 기다리며 영원히 멈춤 X
synchronized(lock1) {
synchronized(lock2) { ... } // Thread A
}
synchronized(lock2) {
synchronized(lock1) { ... } // Thread B
}
해결 방법
synchronized
class Counter {
private int count = 0;
// 한 번에 하나의 스레드만 실행 가능
public synchronized void increment() {
count++; // O 안전
}
}
AtomicInteger
// synchronized보다 가벼운 원자적 연산
class Counter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // O 원자적 연산 보장
}
}
volatile
// 변수를 CPU 캐시가 아닌 메인 메모리에서 직접 읽기/쓰기
class Flag {
private volatile boolean running = true; // 스레드 간 최신값 보장
public void stop() { running = false; }
public boolean isRunning() { return running; }
}
동시성 도구 비교
| 도구 | 특징 | 사용 상황 |
|---|---|---|
synchronized | 간단, 무거움 | 복잡한 임계 구역 |
AtomicInteger | 가벼움, 단순 연산 | 카운터, 플래그 |
volatile | 가시성만 보장 | 단순 읽기/쓰기 |
ReentrantLock | 유연한 락 제어 | 타임아웃, 조건 필요 시 |
ConcurrentHashMap | 스레드 안전 Map | 동시 Map 접근 |
Java에서의 스레드
// 스레드 생성
Thread thread = new Thread(() -> {
System.out.println("별도 스레드 실행");
});
thread.start();
// ExecutorService (실무 권장)
ExecutorService executor = Executors.newFixedThreadPool(4);
executor.submit(() -> {
System.out.println("스레드풀에서 실행");
});
executor.shutdown();
동시성의 핵심은 "공유 자원에 여러 스레드가 동시에 접근할 때 발생하는 문제를 어떻게 제어하느냐" 입니다. 무조건 synchronized를 남발하면 성능이 저하되므로, 상황에 맞는 도구 선택이 중요합니다.