동시성과 병렬성
- 동시성은 여러 작업이 “논리적으로 동시에 진행되는 것처럼 보이도록” 스케줄링하는 개념이고, 병렬성은 실제로 여러 작업이 같은 시점에 물리적으로 동시에 실행되는 상태를 말합니다.[1][3]
- 예를 들어 단일 코어 CPU에서 여러 스레드를 번갈아 실행해 사용자에게 동시에 돌고 있는 것처럼 보이면 동시성이고, 멀티 코어에서 서로 다른 코어가 각각의 스레드를 진짜 동시에 실행하면 병렬성입니다.[3][1]
“동시성은 시간 분할로 번갈아 실행해서 동시에처럼 보이게 하는 것이고, 병렬성은 멀티 코어 위에서 실제로 동시에 실행되는 것입니다.”
Thread-safe와 동시성 이슈
- Thread-safe하다는 것은 여러 스레드가 동시에 같은 객체나 함수에 접근하더라도, 데이터가 꼬이거나 잘못된 결과가 발생하지 않도록 설계·구현되어 있다는 의미입니다.[7][9]
- 자바에서 대표적인 동시성 이슈는 레이스 컨디션, 가시성 문제(메모리 가시성), 원자성(atomicity) 깨짐, 데드락 등이 있으며, 공유 mutable 상태를 어떻게 보호하느냐가 핵심입니다.[9][7]
면접용 포인트: “Thread-safe = 여러 스레드가 동시에 접근해도 상태 일관성이 깨지지 않는 설계/구현”이라고 짧게 말하고, 바로 레이스 컨디션 예시 하나를 붙여주면 좋습니다.
가시성 문제와 원자성 문제
- 가시성 문제는 한 스레드가 공유 변수 값을 변경했는데, 다른 스레드가 그 최신 값을 보지 못하고 자신의 캐시/레지스터에 남은 옛 값으로 계속 동작하는 상황입니다.[7]
- 원자성 문제는 ++, += 같은 복합 연산이 실제로는 “읽기 → 계산 → 쓰기” 여러 단계로 나뉘는데, 이 사이에 다른 스레드가 끼어들어 연산이 중간에 섞여 버리는 문제를 말합니다.[9][7]
"가시성은 ‘변경이 다른 스레드에 보이는가’ 문제, 원자성은 ‘연산이 쪼개지지 않고 한 번에 수행되는가’ 문제입니다.”
자바 동시성 이슈 해결 방법
- 전통적인 방법으로는 synchronized 블록/메서드, wait/notify, volatile, 그리고 java.util.concurrent의 락(ReentrantLock), Condition, ConcurrentHashMap 같은 고수준 동시성 컬렉션을 사용합니다.[7]
- 더 나아가 ExecutorService, 스레드 풀, Future/CompletableFuture 등으로 스레드 직접 생성 대신 태스크 기반으로 동시성을 관리하고, 공유 상태를 줄여서 문제를 근본적으로 완화하는 방향이 많이 사용됩니다.[7]
volatile 키워드
- volatile은 변수에 대한 “메모리 가시성”을 보장해서, 한 스레드가 쓴 값을 다른 스레드가 즉시 메인 메모리에서 읽도록 강제하는 키워드입니다.[7]
- 다만 volatile은 읽기/쓰기 자체만 원자적으로 만들 뿐, 복합 연산(증가, 조건 검사 + 변경 등)에 대한 원자성을 보장해주지는 못하므로, 카운터 증가 같은 곳에는 적합하지 않습니다.[7]
“volatile은 가시성을 해결해 주지만, 동기화(mutex) 대신이 될 수는 없습니다.”
synchronized 키워드와 구현·문제점
- synchronized는 한 번에 하나의 스레드만 진입할 수 있는 임계 구역(critical section)을 만들어서, 객체 모니터(모니터 락)를 기준으로 상호 배제를 보장합니다.[7]
- 내부적으로는 객체마다 모니터 락이 있고, 진입 시 락을 획득하고, 블록/메서드가 끝나면 락을 해제하며, 이 과정에서 OS의 락/뮤텍스 또는 JVM 레벨 최적화(경량 락, 편향 락 등)를 사용합니다.[7]
문제점 관점에서 말할 것들:
- 락 경합이 심해지면 컨텍스트 스위칭 비용이 커지고 성능이 떨어진다.[7]
- 블로킹 방식이라, 락을 기다리는 동안 스레드가 멈춰 있게 되고 데드락·기아(starvation) 위험도 있다.[7]
“synchronized는 모니터 락 기반 상호 배제라서 간단하지만, 블로킹과 락 경합으로 인한 성능 저하, 데드락 위험이 문제라서 고수준 동시성 API와 함께 적절히 써야 합니다.”
atomic의 의미와 Atomic 타입
- atomic하다는 것은 어떤 연산이 중간 상태를 외부에 노출하지 않고 “쪼개지지 않는 하나의 단위”로 실행된다는 뜻입니다.[7]
- 자바의 java.util.concurrent.atomic 패키지에는 AtomicInteger, AtomicLong, AtomicReference 등 CAS(Compare-And-Swap) 기반으로 원자적인 읽기·갱신을 제공하는 타입들이 있습니다.[7]
“AtomicInteger의 incrementAndGet은 내부적으로 CAS를 사용해 다른 스레드와 섞이지 않는 원자적 증가를 제공합니다.”
가시성 문제 심화 질문 포인트
- 여러 스레드가 같은 CPU의 캐시만 사용한다면 가시성 문제는 줄어들 수 있지만, 실제 환경에서는 여러 코어와 각각의 캐시, 재정렬, 컴파일러 최적화 등 요인 때문에 “항상 안전하다”고 볼 수 없습니다.[3][7]
- 그래서 자바 메모리 모델(JMM)은 volatile, synchronized, final 등으로 happens-before 관계를 정의하고, 이 규칙을 통해 스레드 사이의 가시성과 순서를 보장합니다.[7]
“하나의 CPU 캐시만 쓴다는 가정 자체가 비현실적이고, 재정렬까지 고려해야 해서 언어 차원의 메모리 모델 도움 없이 직접 가시성을 보장하기는 매우 어렵습니다.”
CAS 알고리즘 개념
- CAS(Compare-And-Swap)는 “현재 값이 기대한 값과 같을 때만 새로운 값으로 바꾸는” 원자적 연산으로, 실패하면 다시 읽고 재시도하는 방식입니다.[7]
- 이 방식은 락을 쓰지 않고도 경쟁 조건을 제어할 수 있어 lock-free 자료구조 구현에 많이 사용되지만, 충돌이 심하면 반복 재시도로 인해 비용이 커질 수 있습니다.[7]
“CAS는 기대값 비교 후 조건부 교체를 통해 락 없이 원자적 갱신을 보장하는 알고리즘이고, 자바 Atomic 클래스들이 이를 활용합니다.”
Synchronized 컬렉션 vs 동시성 컬렉션
- Vector, Hashtable, Collections.synchronizedXXX는 전체 메서드에 synchronized를 걸어 한 번에 하나의 스레드만 접근하게 하므로, 락 경합이 많고 병렬성이 떨어지는 것이 단점입니다.[7]
- CopyOnWriteArrayList는 쓰기 시 새 배열을 복사하고, 읽기는 락 없이 기존 배열 스냅샷을 보는 구조라서 “읽기 위주” 환경에 적합하고, ConcurrentHashMap은 버킷/세그먼트 단위 락 또는 CAS로 동시성을 극대화합니다.[7]
질문별 키워드 정리:
참고 블로그:
1
2
3
4
5
6
7
8
9
10