Java/JVM
[JVM 밑바닥까지 파헤치기] 12-1. 자바 메모리 모델과 스레드: 동시성
noahkim_
2024. 12. 25. 23:35
저우즈밍 님의 "JVM 밑바닥까지 파헤치기" 책을 정리한 포스팅 입니다
1. 컴퓨터가 여러 작업을 동시에 수행하는 이유
- 연산 성능과 저장 및 통신 성능의 격차가 크기 떄문
- ✅ 프로세서가 요청한 자원의 대기 시간을 활용
- ✅ 서버는 여러 클라이언트 요청을 동시에 처리하므로 동시 처리가 필수적
동시성 프로그래밍의 어려움
- 스케줄링 비결정성: 스레드 실행 순서는 매번 달라짐 (재현이 어려움)
- 공유 메모리 문제: 가시성, 원자성, 순서 등으로 인해 예상치 못한 결과가 발생함
스케줄링 비결정성 문제) 실행 순서
더보기
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5; i++) System.out.println("Thread-1 : " + i);
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5; i++) System.out.println("Thread-2 : " + i);
});
t1.start();
t2.start();
- 실행할 때마다 순서가 바뀜
- 어떤 스레드가 먼저 CPU를 잡을지는 OS 스케줄러에 의해 결정됨
공유 메모리 문제) 가시성
더보기
- 한 스레드가 값을 써도, 다른 스레드가 즉시 그 값을 보지 못할 수 있음
- ⚠️ 각 CPU 코어가 가진 자기 캐시값이 즉시 반영되지 않은 경우
- ⚠️ 다른 코어와의 동기화가 늦을 경우
공유 메모리 문제) 원자성
더보기
int sum = 0;
List<Integer> list = IntStream.range(1, 1001).boxed().toList();
list.parallelStream().forEach(i -> sum += i);
System.out.println(sum);
- java의 명령어 하나는 여러 기계어로 구성되어 있음
- ✅ CPU 스케줄러는 기계어 단위로 원자성을 보장함
- ⚠️ 한 스레드의 연산 중 중단 상태에서 컨텍스트 스위치가 발생하여 다른 스레드에 의해 연산될 경우 값이 꼬일 수 있음
- ⚠️ 원자성을 보장하지 못하므로 레이스 컨디션이 발생함
AtomicInteger sum = new AtomicInteger(0);
list.parallelStream().forEach(i -> sum.addAndGet(i));
- ➡️ 중간 상태가 끼어들지 못하도록 CAS 기반의 원자 연산을 해야 함
공유 메모리 문제) 순서
더보기
- 코드에 적힌대로 작업하지 않는 경우가 발생할 수 있음
- ⚠️ CPU/JIT 연산의 성능을 위해 명령 실행 순서가 변경될 수 있음
동시성 프로그래밍 지원
| 구분 | 제공 기능 | 설명 | 해결하는 문제 |
| 하드웨어 추상화 | OS 스레드 래핑 | JVM이 OS 네이티브 스레드를 감싸서 사용 | OS 의존성 감소 |
| Thread API | Thread, Runnable, Callable 제공 | 스레드 생성/실행 단순화 | |
| 스케줄링 지원 | JVM이 OS 스케줄러와 연동 | 개발자가 직접 관리 불필요 | |
| 스레드 관리 자동화 | ThreadPool | 스레드 재사용 및 작업 큐 관리 | 과도한 스레드 생성 비용 방지 |
| ExecutorService | 작업 제출 중심 API | 스레드 직접 관리 제거 | |
| ThreadPoolExecutor | 세밀한 풀 설정 가능 | 서버 환경 최적화 | |
| 메모리 모델 제공 (JMM) | 가시성 보장 | volatile 키워드 | 캐시 불일치 문제 해결 |
| 원자성 보장 | synchronized, Atomic* | 연산 중간 개입 방지 | |
| 순서 보장 | Happens-Before 규칙 | 명령 재정렬 문제 해결 | |
| 고수준 동시성 API | 스레드 세이프 컬렉션 | ConcurrentHashMap, CopyOnWriteArrayList | 직접 동기화 실수 방지 |
| 원자 변수 | AtomicInteger, AtomicLong 등 | CAS 기반 락 없는 연산 | |
| Lock API | ReentrantLock 등 | synchronized보다 유연한 제어 | |
| 동기화 도구 | CountDownLatch, Semaphore, CyclicBarrier | 작업 간 협업 제어 | |
| 비동기 처리 | CompletableFuture | 논블로킹, 비동기 흐름 제어 |
2. 하드웨어의 효율과 일관성
캐시의 필요성
- 메모리 I/O는 연산작업에 비해 매우 느림
- 이를 보완하고자 메모리와 CPU 사이의 계층을 둠
- 필요한 데이터를 캐시에 복사해 두어 작업을 빠르게 수행
- 작업이 완료되면 결과 데이터를 캐시에서 메모리로 동기화
공유 메모리 멀티프로세스 시스템
- 프로세서별 캐시는 각각 존재하므로 자신의 캐시를 기준으로 연산할 경우, 데이터 불일치가 발생할 수 있음
- ➡️ 캐시 일관성 프로토콜로 공유 공간인 메인 메모리와 동기화를 수행함
- ✅ MSI, MOSI, MESI, Synapse, Firefly
비순차 실행 최적화
- 프로세서가 명령어를 실행하는 순서가 입력 코드에 기술된 명령어 순서와 다를 수 있음
- 프로세서의 컴퓨팅 능력을 끌어냄