티스토리 뷰
📖 동시성과 병렬성
동시성(논리적 개념)
하나의 시스템이 여러 작업을 동시에 처리하는 것처럼 보이게 하는 것, 실질적으로는 한 번에 하나의 작업만을 처리한다.

병렬성(물리적 개념)
여러 작업을 실제로 동시에 처리하는 것. 작업들은 각각이 독립적으로 실행되며 서로 영향을 주지 않는다.

📖 동시성 이슈 해결
synchronized 키워드 사용
synchronized 키워드는 자바에서 가장 기본적인 동기화 방식이다. 여러 스레드가 동시에 공유 자원에 접근하는 경우, 해당 메서드나 코드 블록에 synchronized를 사용하면 한 번에 하나의 스레드만 접근할 수 있도록 락을 건다.
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
volatile 키워드 사용
volatile 키워드는 변수의 값을 캐시하지 않고 항상 메인 메모리에서 읽도록 보장한다. 이를 통해 스레드 간의 가시성 문제를 해결할 수 있지만, count++ 같은 복합 연산은 원자성이 보장되지 않아 synchronized나 Atomic 클래스가 필요할 수 있다.
public class Flag {
private volatile boolean running = true;
public void stop() {
running = false;
}
public void run() {
while (running) {
// 실행 중
}
}
}
동시성 컬렉션 사용 (java.util.concurrent)
java.util.concurrent 패키지의 동시성 컬렉션은 멀티스레드 환경에서도 안전하게 사용할 수 있도록 설계된 자료구조이다. 예를 들어, ConcurrentHashMap은 내부적으로 세그먼트를 나누어 부분 락을 사용하기 때문에 성능도 좋고 안정성도 확보된다.
import java.util.concurrent.ConcurrentHashMap;
public class Example {
private ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
public void putValue(String key, String value) {
map.put(key, value);
}
public String getValue(String key) {
return map.get(key);
}
}
Atomic 클래스 사용 (java.util.concurrent.atomic)
java.util.concurrent.atomic 패키지의 Atomic 클래스는 락 없이도 원자 연산을 보장한다. 내부적으로는 CAS(Compare-And-Swap) 기법을 사용하여 경합을 줄이고 성능을 높인다. AtomicInteger를 사용하면 count++ 연산을 안전하게 처리할 수 있다.
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
* CAS: 멀티스레드 환경에서 락 없이 값을 변경하기 위한 원자적 연산
Lock 객체 사용 (ReentrantLock)
ReentrantLock은 synchronized보다 유연한 락 제어가 가능한 클래스이다. 락 획득 시도를 할 수 있고, 타임아웃을 줄 수도 있으며, 인터럽트가 가능한 등 제어권이 많다. 단, 락을 직접 해제해야 하기 때문에 finally 블록에서 반드시 unlock()을 호출해야 한다.
import java.util.concurrent.locks.ReentrantLock;
public class LockCounter {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
return count;
}
}
📖 Synchronized
💡 synchronized 키워드란?
Synchronized 우리 말로 '동기화된'이라는 뜻이다. 자바에서의 Synchronized는 말 그대로 공유될 가능성이 존재하는 자원에 대해 동기화를 제어할 수 있게 해주는 키워드이다. 자바에서 해당 키워드를 붙일 경우 해당 자원에 대해 오직 하나의 스레드만 접근할 수 있다. 어떠한 스레드가 해당 자원에 접근 중이고 다른 스레드가 같은 자원에 접근하려고 할 경우 접근을 대기시킨다.
//메서드에 사용
public synchrozied void incrementCount() {
this.count++;
}
//코드 블록에 사용
public void incrementCount() {
synchronized(this) {
count++;
}
}
💡 synchronized의 문제점
✔️ 성능 저하
synchronized는 한 번에 하나의 스레드만 임계 영역에 진입할 수 있도록 하기 때문에, 멀티스레드 환경에서는 병렬 처리의 이점이 줄어들고 처리 속도가 느려질 수 있다.
✔️ 데드락
synchronized 블록 안에서 다른 락을 가진 메서드나 객체에 접근할 경우, 두 스레드가 서로 상대방의 락이 풀리기를 기다리며 영원히 대기 상태에 빠지는 데드락 상황이 발생할 수 있다.
💡 내부 구현
자바의 synchronized는 JVM이 제공하는 모니터(Monitor)를 기반으로 구현된다. 모든 객체는 하나의 모니터를 가지고 있으며, synchronized 키워드는 이 모니터를 획득하고 해제하는 방식으로 동기화를 수행한다.
synchronized(obj) {
// 임계 영역
}
📖 Thread-Safe의 의미
Thread-Safe
✔️ 멀티 스레드 프로그래밍에서 일반적으로 어떤 함수나 변수, 혹은 객체가 여러 스레드로부터 동시에 접근이 이루어져도 프로그램의 실행에 문제가 없음을 뜻한다.
✔️ 하나의 함수가 한 스레드로부터 호출되어 실행중일 때, 다른 스레드가 그 함수를 호출하여 동시에 함께 실행되더라도 각 스레드에서의 함수의 수행 결과가 올바르게 나오는 것을 말한다.
Thread-Safe을 지키기 위한 4가지 방법
- Mutual Exclusion(상호 배제)
- 한 시점에 하나의 스레드만 공유 자원에 접근하도록 제한하는 것
- synchronized, Lock, Semaphore
- Atomic Operation(원자 연산)
- 중간에 끊기지 않고 한 번에 수행되는 연산
- AtomicInteger.incrementAndGet()
- Thread-Local Storage(스레드 지역 저장소)
- 스레드마다 자기만의 데이터 공간을 갖도록 하는 방법
- ThreadLocal<T>
- Re-Entrancy(재진입성)
- 한 스레드가 이미 점유 중인 락을 자기 자신이 다시 획득할 수 있는 성질
- ReentrantLock, synchronized
📖 가시성과 원자성
가시성 문제(Visibility Problem)
가시성 문제는 멀티스레드 프로그래밍에서 한 스레드가 변경한 변수값이 다른 스레드에게 즉시 보이지 않는 현상을 의미한다. 이는 각 스레드가 독립적인 CPU 캐시를 사용하기 때문인데, 한 스레드가 메모리의 값을 변경해도 다른 스레드가 그 변경된 값을 즉시 보지 못할 수 있다. 이러한 상황은 데이터의 일관성을 무너뜨릴 수 있다.
volatile 키워드는 변수의 가시성을 보장하는 데 사용된다. volatile로 선언된 변수는 항상 메인 메모리에서 값을 읽고 쓰도록 보장되며, 어떤 쓰레드가 volatile 변수를 수정하면 그 변경 사항이 즉시 모든 스레드에 반영된다.
💡 추가
여러 스레드가 모두 하나의 CPU 코어에서 실행되고, 같은 캐시 메모리를 공유한다면 실제로 가시성 문제는 거의 발생하지 않을 수 있다. 하지만 현실의 대부분 시스템은 멀티코어 구조이기 때문에 스레드가 서로 다른 코어에서 실행될 수 있고, 각 코어는 자기만의 캐시를 갖고 있어서 한 스레드가 변경한 값이 다른 스레드에게 바로 전달되지 않을 수 있다. 이처럼 각 코어의 캐시가 서로 다른 값을 갖고 있을 수 있기 때문에 가시성 문제가 발생하며, 자바에서는 이를 방지하기 위해 volatile이나 synchronized 같은 키워드로 메모리 동기화를 강제해줘야 한다.
원자성 문제(Atomicity Problem)
원자성 문제는 멀티스레드 환경에서 하나의 연산이 여러 단계로 나뉘어 수행되기 때문에, 그 중간에 다른 스레드가 끼어들면서 데이터 충돌이 발생하는 문제를 말한다. 예를 들어 count++는 단순해 보이지만 실제로는 “읽기 → 계산 → 저장”의 세 단계로 나뉘며, 이 사이에 다른 스레드가 접근하면 결과가 꼬일 수 있다. 이를 해결하기 위해 자바에서는 synchronized 키워드로 임계 영역을 보호하거나, AtomicInteger와 같은 원자성 보장 클래스를 사용하여 락 없이 안전한 연산을 수행할 수 있도록 지원한다.
📖 Atomic
Atomic 하다?
원자적(atomic)이라는 말은 더 이상 나눌 수 없는 최소 단위의 연산이라는 의미를 가지고 있다. 즉, 어떤 스레드가 count++ 같은 연산을 수행할 때, 그 연산이 도중에 다른 스레드에 의해 끼어들 수 없다면 그 연산은 atmoic 하다고 말한다.
Atomic 타입
자바에서 제공하는 java.util.concurrent.atomic 패키지의 클래스들이 대표적인 atomic 타입이다. 대표적인 atomic 타입은 다음과 같다.
- AtomicInteger
- AtomicLong
- AtomicBoolean
- AtomicReference<T>
'Study > CS 스터디 - Java' 카테고리의 다른 글
| JVM & GC (0) | 2025.04.09 |
|---|---|
| Thread (0) | 2025.03.27 |
| 컬렉션 (0) | 2025.03.27 |
| 람다, 스트림, 어노테이션 (0) | 2025.03.20 |
| 문자열, 예외, 제네릭 (0) | 2025.03.19 |