Replies: 3 comments
-
|
저는 이 세 가지를 "동시성을 다루는 도구"로 묶어서 보지만, 각각이 해결하는 문제와 보장하는 범위가 다르다고 생각합니다. 흔히 "셋 다 동시성 도구"라고 뭉뚱그리는데, 사실 동시성 문제는 크게 (1) 가시성(visibility) 문제와 (2) 원자성(atomicity) 문제 두 가지로 나뉘고, 셋은 각자 다른 문제를 풀거나 다른 깊이로 풉니다. 그래서 "어느 게 더 좋은가"가 아니라 내가 풀려는 동시성 문제가 가시성인가 원자성인가, 1. 먼저 풀어야 할 문제 — 가시성과 원자성세 도구를 비교하기 전에 무엇을 해결해야 하는지부터 명확히 해야 합니다. (a) 가시성(Visibility) 문제JVM에서 각 스레드는 성능을 위해 메인 메모리의 값을 CPU 캐시(또는 레지스터)에 복사해서 사용합니다. 그래서 한 스레드가 변수를 바꿔도, 다른 스레드는 자기 캐시에 있는 옛 값을 계속 볼 수 있습니다. class StopFlag {
private boolean stop = false;
public void runUntilStopped() {
while (!stop) { // 다른 스레드가 stop=true로 바꿔도 영원히 못 볼 수 있음
// 작업
}
}
public void stopIt() {
stop = true;
}
}이게 가시성 문제입니다. 한 스레드의 변경이 다른 스레드에 보이는가의 문제. (b) 원자성(Atomicity) 문제겉보기엔 한 줄짜리 연산이지만 실제로는 여러 단계로 쪼개지는 경우가 있습니다. counter++; // 사실은 3단계: 읽기(read) → 증가(modify) → 쓰기(write)스레드 A가 이게 원자성 문제입니다. 여러 단계로 이루어진 연산이 중간에 끼어듦 없이 한 번에 끝나는가의 문제. (c) 두 문제의 관계
세 도구는 이 두 문제를 다른 방식·다른 범위로 풉니다. 2. volatile — 가시성만, 원자성은 안 함
class StopFlag {
private volatile boolean stop = false; // ← 추가
public void runUntilStopped() {
while (!stop) { // 이제 다른 스레드의 변경이 즉시 보임
// 작업
}
}
}보장하는 것
보장하지 않는 것
volatile int counter = 0;
// 1000개 스레드가 동시에 counter++를 1000번씩 호출하면
// 결과는 1,000,000이 아니라 그보다 적게 나옴언제 쓰는가저는 volatile이 적합한 경우를 두 가지로 봅니다.
// volatile이 잘 어울리는 예
private volatile boolean initialized = false;
public void initialize() {
// ... 초기화 로직 ...
initialized = true; // 단순 대입
}
public void doWork() {
if (!initialized) return;
// ...
}왜 빠른가volatile은 락이 아닙니다. 단지 메모리 접근 방식만 바꿉니다. 그래서 락 획득·해제 비용이 없고 매우 빠릅니다. 다만 그 대가로 원자성을 못 줍니다. 3. synchronized — 가시성 + 원자성, 가장 강한 보장
class Counter {
private int count = 0;
public synchronized void increment() {
count++; // 한 번에 한 스레드만 실행
}
public synchronized int getCount() {
return count;
}
}또는 블록 단위로: public void doSomething() {
synchronized (this) {
// 임계 구역
}
}보장하는 것
보장의 강력함세 도구 중 가장 강한 보장을 줍니다. 여러 줄의 코드를 묶어 한 번에 보호할 수 있다는 게 핵심입니다. public synchronized void transfer(Account from, Account to, int amount) {
from.withdraw(amount); // 1단계
to.deposit(amount); // 2단계
// 두 단계가 항상 함께 일어나거나 함께 안 일어남
}이건 volatile이나 단일 Atomic으로는 불가능합니다. 복합 연산의 원자성이 필요할 때는 synchronized가 유일한 답인 경우가 많습니다. 비용
자바 1.6 이후의 개선옛날에는 synchronized가 무거운 락이라 권장되지 않았지만, 자바 1.6 이후 JVM이 biased locking, lightweight locking, lock coarsening 같은 최적화를 적용해서 경합이 적은 경우는 거의 비용이 없습니다. "synchronized는 무조건 느리다"는 이제 옛말입니다. 4. Atomic 클래스 — 원자성 + 가시성, 락 없이 (대부분)
class Counter {
private final AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // 원자적으로 증가
}
public int getCount() {
return count.get();
}
}보장하는 것
어떻게 락 없이 가능한가 — CAS내부적으로 CAS (Compare-And-Swap) CPU 명령을 사용합니다.
int current;
do {
current = get(); // 현재 값 읽기
} while (!compareAndSet(current, current + 1)); // 안 바뀌었으면 +1로 갱신, 바뀌었으면 재시도이걸 낙관적 동시성 제어라고 부릅니다. 락을 잡지 않고 "내가 본 값이 안 바뀌었으면 진행, 바뀌었으면 다시"라는 식입니다. 이전 트랜잭션 락 토론에서 다룬 낙관적 락과 같은 정신입니다. 장점
한계
// ❌ 이건 원자적이지 않다
if (counter.get() < 100) {
counter.incrementAndGet(); // 그 사이에 다른 스레드가 100을 넘겼을 수 있음
}이런 복합 연산은 결국 synchronized가 필요합니다. 5. 세 도구의 비교표
중요한 함의이 표가 보여주는 건 세 도구가 "강함 → 약함" 순으로 줄 세워지지 않는다는 점입니다. volatile은 가장 빠르지만 보장이 가장 약하고, synchronized는 가장 강하지만 가장 비싸고, Atomic은 그 중간에서 단일 변수에 특화되어 있습니다. 6. 같은 문제를 셋으로 풀어보면"카운터를 1000개 스레드에서 동시에 1000번씩 증가시키기" 문제를 셋으로 풀어보면 차이가 명확해집니다. volatile — 실패volatile int counter = 0;
// 결과: 999,500 (또는 비슷한 값) — lost update 발생volatile은 가시성만 주고 원자성은 안 줘서 synchronized — 성공, 하지만 느림int counter = 0;
public synchronized void increment() { counter++; }
// 결과: 1,000,000 — 정확
// 다만 모든 스레드가 락을 잡으려고 줄을 서므로 느림Atomic — 성공, 빠름AtomicInteger counter = new AtomicInteger(0);
public void increment() { counter.incrementAndGet(); }
// 결과: 1,000,000 — 정확
// CAS 기반이라 synchronized보다 빠름이 단순한 경우는 Atomic이 최선의 답입니다. 하지만 "카운터가 100 미만일 때만 증가" 같은 복합 조건이 추가되면 Atomic으로는 안 되고 synchronized가 필요해집니다. 7. 미션과 연결지어 보면이번 예약 대기 미션이 단일 서버라면, 세 도구가 등장할 만한 자리는 다음과 같다고 생각합니다. 메모리 캐시 / 통계 카운터 → Atomic예약 통계, 조회수 같은 단순 카운터라면 AtomicLong이 자연스럽습니다. 플래그 → volatile"초기화 완료", "셧다운 시작" 같은 단순 플래그가 있다면 volatile. 복합 도메인 동시성 → synchronized보다는 DB로 위임"예약 취소 + 첫 대기 승격"이라는 복합 동작은 두 객체의 상태를 함께 바꿔야 합니다. 자바 레벨의 synchronized로 보호할 수도 있지만, 저는 이번 미션 범위에서는 DB UNIQUE 제약 + 트랜잭션으로 위임하는 게 더 자연스럽다고 생각합니다. 이유는 (1) 다중 서버로 확장될 때 자바 synchronized는 무력하고, (2) DB가 이미 동시성을 잘 다루는 도구를 가지고 있고, (3) 사이클1 토론에서 정리한 "어떤 규칙은 DB가 들고 있는 게 옳다"의 원칙과 맞기 때문입니다. 자바 레벨 동시성 도구가 정말 필요한 자리
8. 선택 기준 — 어떤 신호를 보고 고를까저는 이렇게 판단합니다. 가시성만 필요하고 단순 대입만 일어난다 → volatile
단일 변수의 원자적 read-modify-write가 필요하다 → Atomic
복합 연산(여러 줄, 여러 변수)의 원자성이 필요하다 → synchronized
둘 이상의 도구가 필요하다고 느낀다면, 그건 신호다
위 도구로 안 되는 영역
9. 정리
핵심 정리:
미션 관점에서 보면, 예약·대기 같은 DB 데이터의 동시성은 자바 도구보다 DB UNIQUE 제약 + 트랜잭션에 위임하는 게 보통 더 안전합니다. 자바 동시성 도구는 메모리 캐시·통계·플래그 같은 자바 객체 상태에 어울리는 도구라고 생각합니다. 결국 셋 중 무엇이 더 좋은가가 아니라, 풀려는 동시성 문제의 성격에 맞는 도구를 고르는 일이라고 봅니다. 그 성격을 정확히 진단하는 게 도구를 외우는 것보다 중요합니다. |
Beta Was this translation helpful? Give feedback.
-
|
1. synchronizedsynchronized는 특정 코드 영역에 한 번에 하나의 스레드만 접근하도록 제한하는 방식입니다.
사용 예시:
2. volatilevolatile은 변수 값을 메인 메모리에서 읽고 쓰도록 하여 가시성을 보장합니다.
사용 예시:
3. AtomicAtomic은 CAS 기반으로 단일 변수에 대한 연산을 원자적으로 처리해주는 클래스입니다.
사용 예시:
|
Beta Was this translation helpful? Give feedback.
-
|
세가지 모두 멀티스레드 환경에서 공유 변수를 안전하게 다루기 위한 도구입니다. 먼저 동시성 문제에는 크게 3가지의 보장으로 나눠볼 수 있습니다. 가시성 Visibility한 스레드가 바꾼 값을 다른 스레드가 즉시 볼 수 있는가? 원자성 Atomicity복합 연산 (읽기 → 수정 → 쓰기)이 중간에 끼어들지 않고 한 덩어리로 실행이 되는가? 순서 Ordering컴파일러의 명령어 재배치로 인해 코드 순서가 뒤바뀌지 않는가? 이러한 동시성 문제들을 3가지 도구가 어떤 차이점으로 동작이 되는지 설명하겠습니다. volatile
private volatile boolean running = true; // 플래그 가시성 보장synchronized
public synchronized void increment() { count++; } // 원자성+가시성
// 또는
synchronized (lock) { /* 임계 영역 */ }Atomic*
private AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // 락 없이 원자적 +1 |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
synchronizedvsvolatilevsAtomic차이에 대해서 설명해주세요Beta Was this translation helpful? Give feedback.
All reactions