[Java 동시성] 동시성 문제 해결 방안
Java에서 멀티스레드 환경의 동시성 문제를 해결하기 위한 메커니즘을 설명합니다.
다양한 방안이 있지만 그 중 synchronized, database lock(비관적, 낙관적 락) 을 설명 + volatile, atomic
synchronized 키워드
synchronized를 명시해주면 하나의 스레드만 접근 가능
현재 데이터를 사용하고 있는 해당 스레드를 제외하고 나머지 스레드들은 데이터 접근을 막아 순차적으로 데이터에 접근할 수 있도록 해줌
단, 하나의 프로세스 안에서만 보장되기 때문에 서버가 여러대(여러JVM) 일 경우에는 여전히 문제 => DB Lock으로 해결
메소드 동기화
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
블록 동기화
public void increment() {
synchronized(this) {
count++;
}
}
Database Lock
Database Lock이란 데이터베이스에서 한 프로세스가 데이터베이스 자원(데이터)를 잠그면 다른 다른 프로세스는 잠긴 데이터를 변경할 수 없다.
- 여러 JVM 간의 동시성 제어 가능
- 트랜잭션 격리 수준 제공
- 데이터 무결성 보장
- 무결성 : 데이터베이스에 저장된 데이터 값과 그것이 표현하는 현실 세계의 실제 값이 일치하는 정확성
-
무결성 제약 조건(Constraint)은 데이터베이스에 저장된 데이터의 정확성을 보장하기 위해 정확하지 않은 데이터가 데이터베이스 내에 저장되는 것을 방지하기 위한 제약조건
-
NULL 무결성
- 특정 속성 값이 NULL이 될 수 없도록 제한.
-
고유(Unique) 무결성
- 특정 속성 값이 중복되지 않도록 제한.
- 예: 고객번호 속성은 각 튜플에서 고유.
-
도메인(Domain) 무결성
- 속성 값은 정의된 도메인 내 범위에서만 가지도록 제한.
- 예: 성별 속성은 ‘남’ 또는 ‘여’만 가능.
-
키(Key) 무결성
- 하나의 릴레이션에 적어도 하나의 키가 반드시 존재.
-
관계(Relationship) 무결성
- 튜플 삽입 가능 여부나 릴레이션 간의 관계 적절성을 제한.
-
참조(Referential) 무결성
- 외래키는 NULL이거나 참조 릴레이션의 기본키와 동일해야 함.
-
개체(Entity) 무결성
- 기본키를 구성하는 속성 값은 NULL이 될 수 없음.
-
NULL 무결성
크게 2가지 - 비관적 락, 낙관적 락
크게 2가지 “비관적 락, 낙관적 락” 사용
- 비관적 락:
FOR UPDATE
는 해당 레코드를 즉시 락으로 걸어 다른 트랜잭션이 수정하지 못하도록 막음 - 낙관적 락: 데이터 레코드에 버전(version)이라는 필드를 추가하여 충돌을 감지.
예로 트랜잭션 시작 시 읽어온 버전 값(예:version = 0
)을 조건으로, 데이터 수정 시 버전이 변경되지 않았는지 확인
-- 예시: 비관적 락 -> select...for update
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- 예시: 낙관적 락
UPDATE users SET version = version + 1
WHERE id = 1 AND version = 0;
차이와 실생활 예시
- 비관적 락 - 사전 예약 시스템: 한 공연의 좌석을 동시에 예약할 경우, 누군가 특정 좌석을 선택하면 다른 사람은 그 좌석에 접근할 수 없게 락을 거는 방식
- 낙관적 락 - 협업 문서 편집: 여러 사용자가 동시에 문서를 편집할 때, 각 사용자가 편집 후 저장할 때 충돌 여부를 확인
특징 | 비관적 락 (FOR UPDATE ) |
낙관적 락 (version ) |
---|---|---|
락 사용 여부 | 트랜잭션 동안 데이터에 락을 걸어 충돌 방지 | 락 없이 충돌 발생 시 처리 |
데이터 충돌 | 충돌 가능성을 사전에 방지 | 충돌 발생 후 이를 감지하여 처리 |
성능 | 다른 트랜잭션이 대기하므로 성능 저하 가능 | 락이 없어 성능 우수 |
사용 시나리오 | 충돌 가능성이 높은 경우 (동시 수정이 빈번) | 충돌 가능성이 낮은 경우 (수정 빈도 낮음) |
volatile 키워드와 Atomic 클래스
특징 | volatile |
Atomic 클래스 |
---|---|---|
목적 | 변수의 가시성 보장 | 변수의 가시성과 원자성 보장 |
복합 작업 지원 | 지원하지 않음 | 지원 (예: 증가, 감소 등) |
락 사용 여부 | 없음 | 없음 (CAS 알고리즘 사용) |
주요 클래스/키워드 |
volatile 키워드 |
AtomicInteger , AtomicLong 등 |
사용 예 | 플래그 변수, 단순 상태 체크 | 카운터, 복합 데이터 연산 |
volatile
volatile
은 변수의 값을 여러 스레드가 메모리 간 동기화하여 읽고 쓰도록 보장
volatile
변수를 읽거나 쓸 때, CPU 캐시(스레드 스택) 대신 메인 메모리에서 값을 사용
class VolatileExample {
private static volatile boolean running = true;
public static void main(String[] args) {
Thread worker = new Thread(() -> {
while (running) { // running 값이 메모리에서 항상 최신 값으로 읽힘
System.out.println("Working...");
}
System.out.println("Stopped.");
});
worker.start(); //main thread는 바로 아래 단계로 지속.
try {
Thread.sleep(1000); // 1초 후에 running을 false로 설정
} catch (InterruptedException e) {
e.printStackTrace();
}
running = false; // 메인 메모리에서 모든 스레드가 최신 값을 읽음
}
}
AtomicXXX
Atomic
클래스는 멀티스레드 환경에서 원자적(Atomic) 연산을 보장
내부적으로 CAS(Compare-And-Swap) 알고리즘을 사용해 Lock 없이 동시성을 처리
- CAS: 스레드 내의 스택(캐시)에 저장된 값과 메인 메모리에 저장된 값을 “비교한 후 같다면 새로운 값으로 치환” 하고, 값이 다르다면 계속 재시도
- Lock을 걸지 않으니까 비용이 작다.
- volatile 처럼 메인 메모리도 사용한다. (실제로 CAS의 내부 구현에서 메인 메모리의 값을 가져오는 용도로 volatile을 사용)
- 읽기 쓰기 연산만 지원하는 volatile과 다르게 Atomic은 복합 연산(증,감) 가능 => 원자성 보장
- 참고: ConcurrentHashMap 클래스에서도 사용되고, 빈 버킷에 값을 넣을 때 CAS 알고리즘을 사용함으로써 빠르게 처리. (이미 값이 들어있으면 synchronized를 사용)
import java.util.concurrent.atomic.AtomicInteger;
class AtomicExample {
private static AtomicInteger counter = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread incrementer1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.incrementAndGet(); // 원자적으로 증가
}
});
Thread incrementer2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.incrementAndGet(); // 원자적으로 증가
}
});
incrementer1.start();
incrementer2.start();
incrementer1.join(); // 스레드 종료 대기
incrementer2.join();
System.out.println("Final Counter Value: " + counter.get());
//출력: Final Counter Value: 2000
}
}
volatile와 Atomic의 차이점 이해 예제
잘못된 volatile
사용 예
class VolatileCounter {
private static volatile int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter++; // 복합 작업 (읽기 + 쓰기) → 원자성이 깨짐
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter++; // 복합 작업 -> counter = counter + 1;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final Counter Value: " + counter);
}
}
Final Counter Value
는 2000이 아닐 수 있다.
이는 counter++
가 읽기 → 수정 → 쓰기로 이루어진 복합 작업이기 때문. volatile
은 원자성을 보장하지 않으므로 데이터 경합이 발생 가능한 것
💡 동시성 제어 선택 기준
- 단일 JVM: synchronized 사용
- 분산 환경: Database Lock 사용
- 성능 중요: volatile 또는 atomic 클래스 고려
적절한 동시성 제어 메커니즘 선택은 시스템의 안정성과 성능에 큰 영향을 미칩니다.
댓글남기기