힙(Heap)과 스택(Stack)
힙(Heap)
- 동적 할당 객체(대부분의
new로 생성된 객체)가 저장되는 메모리 영역 - 프로세스(= JVM) 단위로 공유되므로 여러 스레드가 같은 힙을 함께 사용
- 객체는 GC 대상이 되며, 참조가 끊기면 회수 가능
스택(Stack)
- 함수(메서드) 호출 단위로 프레임이 쌓이는 영역
- 스레드마다 별도의 스택을 가짐(스레드 로컬)
- 주로 저장되는 것들
- 지역 변수, 매개변수, 리턴 주소, 임시 값 등
전역 변수는 어디에 저장되나?
- Java 기준으로 “전역 변수”에 가까운 것은 클래스 변수(
static) static필드, 상수 등은 JVM의 메서드 영역(Method Area / Metaspace)에 올라가며 모든 스레드가 공유- (참고) 네이티브/저수준 관점에서는 “데이터 섹션” 같은 표현을 쓰지만, Java/JVM에서는 보통 메서드 영역으로 설명하는 게 정확함
Thread.start() vs run() 차이
start()
- JVM이 새 스레드(실행 흐름)를 만들고, 그 스레드에서
run()을 호출 - 즉, 멀티스레딩 환경이 구성됨
run()
- 단순히 일반 메서드 호출
- 호출한 스레드(보통 main 스레드)에서 그대로 실행됨 → 새 스레드가 생성되지 않음
Thread t = new Thread(() -> System.out.println(Thread.currentThread().getName()));
t.run(); // main에서 실행됨
t.start(); // 별도 스레드에서 실행됨
스레드 상태 (Thread State)

- NEW: 생성만 되었고 아직
start()안 함 - RUNNABLE: 실행 중이거나 실행 가능한 상태(실제로는 OS 스케줄링 대기 포함)
- BLOCKED:
synchronized모니터 락을 얻기 위해 진입 대기 중 - WAITING: 시간 제한 없이 대기 (
Object.wait(),Thread.join()등) - TIMED_WAITING: 시간 제한 대기 (
sleep,wait(timeout),join(timeout)등) - TERMINATED: 실행 완료
join() 함수
- 특정 스레드가 끝날 때까지 기다릴 때 사용 (busy-wait / sleep 반복보다 효율적)
- 호출한 스레드는 WAITING 상태가 됨
- 무한 대기 문제는
join(timeout)으로 완화 → TIMED_WAITING
Thread t = new Thread(task);
t.start();
t.join(); // 끝날 때까지 대기 (WAITING)
t.join(1000L); // 최대 1초만 대기 (TIMED_WAITING)
volatile이 뭐고 왜 쓰는지
핵심 목적: 가시성(visibility) 보장
- 여러 스레드가 공유 변수를 볼 때, 각자 CPU 캐시/레지스터에 의해 최신 값이 안 보일 수 있음
volatile은- 쓰기(write): 다른 스레드가 볼 수 있도록 “즉시” 메모리에 반영되도록
- 읽기(read): 최신 값을 메모리에서 가져오도록
- 또한
volatilewrite → read 사이에 happens-before 관계를 만들어 순서/가시성을 보장
주의: 원자성(atomicity)을 보장하지는 않음
volatile int x; x++는 원자적이지 않음 (읽기-증가-쓰기의 3단계)
적합한 경우
- “플래그” 같은 단순 상태 공유(종료 신호 등)
class StopFlag {
private volatile boolean stop = false;
void requestStop() { stop = true; }
void work() {
while (!stop) { /* do work */ }
}
}
withdraw() 같은 출금 함수의 동시성 문제를 심플하게 해결하려면?
1) synchronized로 임계 구역 보호
- 함수 전체에 걸거나, 문제되는 부분만 블록으로 최소화 가능
class Account {
private int balance = 1000;
public synchronized boolean withdraw(int amount) {
if (balance < amount) return false;
balance -= amount;
return true;
}
// 또는
public boolean withdraw2(int amount) {
synchronized (this) {
if (balance < amount) return false;
balance -= amount;
return true;
}
}
}
위 방법(synchronized)의 단점과 해결 방향
단점
- 경쟁이 심하면 BLOCKED가 늘어나고 성능 저하
- 락을 얻기까지 무한 대기 가능
- 락 획득 대기 중에는 제어 옵션이 제한적(타임아웃, 인터럽트 대응 등)
대안 1) LockSupport 기반 대기 제어
park()/parkNanos()로 대기 시간 제어 가능park는 WAITING/TIMED_WAITING 형태로 대기하며, 설계에 따라 interrupt 처리를 유연하게 구성 가능
LockSupport.parkNanos(1_000_000L); // 1ms 대기
대안 2) ReentrantLock (실무에서 가장 흔한 Lock 구현체)
tryLock()/tryLock(timeout)로 무한 대기 회피lockInterruptibly()로 인터럽트 반응 가능Condition으로wait/notify보다 명확한 조건 대기 구현
ReentrantLock lock = new ReentrantLock();
boolean ok = lock.tryLock(100, TimeUnit.MILLISECONDS);
if (ok) {
try { /* critical section */ }
finally { lock.unlock(); }
}
CAS란?
- Compare-And-Swap: 메모리 값이 예상값과 같으면 새 값으로 바꾸는 하드웨어 원자 연산
- 락 없이도 원자적 갱신이 가능해서 성능이 유리한 경우가 많음(특히 경합이 낮을 때)
- 단, CAS는 재시도 루프가 필요해 경합이 높으면 스핀 비용이 커질 수 있음
Java에서의 예: AtomicInteger
AtomicInteger v = new AtomicInteger(0);
v.incrementAndGet(); // 내부적으로 CAS 기반
Future 객체
- 비동기 작업 결과를 “나중에” 받기 위한 핸들
- 주요 기능
get(): 결과 대기(블로킹)get(timeout): 시간 제한 대기cancel(true): 취소 시도(인터럽트 전달 가능)isDone(),isCancelled()
Future<Integer> f = executor.submit(() -> 1 + 2);
Integer result = f.get(1, TimeUnit.SECONDS);
참고:
Future는 콜백/조합이 불편해서, Java 8+에서는 보통CompletableFuture를 더 자주 사용함.
ThreadPoolExecutor / ExecutorService
왜 쓰나?
- 스레드 생성/종료 비용이 큼 → 스레드를 재사용해서 성능 안정화
- 작업 제출과 실행을 분리하고, 큐/정책으로 부하를 제어
핵심 구성(ThreadPoolExecutor)
corePoolSize: 기본 유지 스레드 수maximumPoolSize: 최대 스레드 수workQueue: 작업 큐(대기열)keepAliveTime: 초과 스레드 유지 시간RejectedExecutionHandler: 포화 시 거절 정책
ExecutorService pool =
new ThreadPoolExecutor(
4, 8,
30, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000)
);
여러 가지 풀 전략
1) Fixed Thread Pool
- 고정된 스레드 수
- CPU 바운드 작업에 자주 사용
Executors.newFixedThreadPool(n);
2) Cached Thread Pool
- 필요 시 스레드 생성, 유휴 시 회수
- 짧은 작업이 많고 폭발적 트래픽에 유리하지만, 과도한 스레드 증가 위험
Executors.newCachedThreadPool();
3) Single Thread Executor
- 단일 스레드로 순차 처리(작업 순서 보장)
Executors.newSingleThreadExecutor();
4) Scheduled Thread Pool
- 지연/주기 작업
Executors.newScheduledThreadPool(n);
5) Work-Stealing / ForkJoinPool
- 작업을 작은 단위로 쪼개 병렬 처리(분할 정복)
- CPU 바운드에 적합
Executors.newWorkStealingPool();
큐/정책 선택 포인트(실무에서 중요)
LinkedBlockingQueue(bounded 권장): 큐가 길어질 수 있음 → 메모리/지연 증가 가능SynchronousQueue: 큐 없이 즉시 핸드오프 → 급격히 스레드가 늘 수 있음- 거절 정책:
AbortPolicy(기본): 예외CallerRunsPolicy: 호출자 스레드가 실행(백프레셔)DiscardPolicy/DiscardOldestPolicy: 유실 가능성
원하면 위 내용을 “한 페이지짜리 블로그 포스트 형태(서론/요약/실무 팁 포함)”로 재편집해서 더 깔끔한 최종본으로도 정리해줄 수 있음.
'김영한의 실전 자바 - 고급 1편' 카테고리의 다른 글
| 김영한의 실전 자바 - 고급 2편(총 정리) (0) | 2026.01.26 |
|---|---|
| 김영한의 실전 자바 - 고급 1편(Executor) (0) | 2025.01.13 |
| 김영한의 실전 자바 - 고급 1편(동시성 컬렉션) (0) | 2025.01.11 |
| 김영한의 실전 자바 - 고급 1편(CAS) (1) | 2025.01.11 |
| 김영한의 실전 자바 - 고급 1편(생산자 소비자 문제: Object - wait/notify, ReentarantLock - await/signal, BlockingQueue) (1) | 2025.01.08 |