Java 멀티스레드에서 충돌 방지하기

멀티스레드 환경에서는
여러 개의 스레드가 동시에 데이터를 변경할 때 경합 조건(Race Condition)이 발생할 수 있다.
이 문제를 해결하기 위해 UUID를 활용한 유일한 키 생성, 원자적 연산(Atomic Operation) 및 동기화 기법을 사용해야 한다.
1. UUID와 nanoTime()을 활용한 고유한 키 생성
멀티스레드 환경에서 유일한 키 값을 생성하는 방법으로는 UUID와 nanoTime()이 있다.
✅ UUID (절대 겹치지 않는 유일한 값)
String uniqueKey = UUID.randomUUID().toString();
System.out.println("생성된 UUID: " + uniqueKey);
- UUID.randomUUID()는 전 세계적으로 중복되지 않는 값을 생성하므로,
- 데이터베이스의 고유 키나 분산 시스템에서 ID를 생성할 때 유용하다.
✅ System.nanoTime() (고유 값 + 순서 비교 가능)
String keyWithTime = UUID.randomUUID().toString() + "_" + System.nanoTime();
System.out.println("생성된 키: " + keyWithTime);
- nanoTime()은 현재 시간을 기반으로 한 유일한 값이지만,
- 동시에 실행되는 스레드가 있으면 중복될 가능성이 있음
- 하지만 값이 증가하는 특징을 활용하여 생성 순서를 비교할 수 있음
💡 따라서 UUID + nanoTime()을 조합하면 "유일한 값 + 생성 순서"를 모두 확보 가능!
2. 멀티스레드 환경에서 경합 조건(Race Condition) 발생 예제
아래 코드는 1000개의 스레드가 count 값을 증가시키는 예제이다.
충돌 방지 처리를 하지 않으면 count 값이 예상과 다르게 나올 수 있다.
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadCollisionExample {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
System.out.println("스레드 충돌방지 시작!");
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
executor.execute(() -> count++); // ⚠ Race Condition 발생 가능
}
executor.shutdown();
Thread.sleep(1000);
System.out.println("최종 count 값: " + count); // ❌ 예상값(1000)보다 작을 수 있음!
}
}
❌ 실행 결과 (충돌 발생)
최종 count 값: 973
- 1000번 증가시켰는데도 정확한 값이 나오지 않음!
- 여러 스레드가 동시에 count++ 연산을 수행하면서 값이 덮어씌워지는 문제 발생
- 이런 문제를 해결하려면 **원자적 연산(Atomic Operation)**을 사용해야 함
3. 충돌 방지 방법
✅ 해결 방법 1: AtomicInteger 사용 (원자적 연산)
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
executor.execute(() -> count.incrementAndGet()); // ✅ 원자적 증가
}
executor.shutdown();
Thread.sleep(1000);
System.out.println("최종 count 값: " + count.get()); // 🎯 정확한 값 나옴
}
🔹 AtomicInteger는 synchronized 없이도 안전하게 동작
🔹 원자적 연산을 사용하여 Race Condition을 방지
✅ 해결 방법 2: synchronized 블록 사용
private static int count = 0;
public static synchronized void increment() {
count++; // ✅ 동기화하여 하나의 스레드만 접근 가능
}
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
executor.execute(() -> increment());
}
executor.shutdown();
Thread.sleep(1000);
System.out.println("최종 count 값: " + count); // 🎯 정확한 값 나옴
}
🔹 synchronized를 사용하면 하나의 스레드만 접근 가능
🔹 하지만 속도가 느려질 수 있음! (여러 스레드가 동시에 접근하지 못함)
✅ 해결 방법 3: ReentrantLock 사용
import java.util.concurrent.locks.ReentrantLock;
private static int count = 0;
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
executor.execute(() -> {
lock.lock(); // ✅ 락 획득
try {
count++;
} finally {
lock.unlock(); // ✅ 락 해제
}
});
}
executor.shutdown();
Thread.sleep(1000);
System.out.println("최종 count 값: " + count); // 🎯 정확한 값 나옴
}
🔹 synchronized보다 더 세밀한 제어 가능
🔹 락을 해제해야 하는 책임이 개발자에게 있음 (주의 필요)
4. 추가적인 충돌 방지 기법
✅ ConcurrentHashMap을 활용한 안전한 데이터 관리
import java.util.concurrent.ConcurrentHashMap;
private static ConcurrentHashMap<String, Integer> data = new ConcurrentHashMap<>();
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
executor.execute(() -> data.put(UUID.randomUUID().toString(), 1));
}
executor.shutdown();
System.out.println("데이터 개수: " + data.size()); // 🎯 정확한 개수 나옴
}
🔹 멀티스레드 환경에서 안전하게 데이터를 저장 가능
🔹 HashMap 대신 ConcurrentHashMap을 사용하면 동기화 문제 해결 가능
5. 결론
특징
|
속도
|
|
AtomicInteger
|
원자적 연산 사용
|
🟢 빠름
|
synchronized
|
한 번에 하나의 스레드만 접근
|
🟡 느릴 수 있음
|
ReentrantLock
|
세밀한 제어 가능
|
🟡 느릴 수 있음
|
ConcurrentHashMap
|
동기화된 해시맵
|
🟢 빠름
|
📌 멀티스레드 환경에서는 AtomicInteger와 ConcurrentHashMap이 가장 적절한 선택!
📌 락을 사용할 경우 반드시 try-finally로 unlock()을 보장해야 함
📌 UUID와 nanoTime을 활용하면 유일한 키를 생성하면서 순서도 비교 가능!
오류나 궁금하신점은
아래 댓글로 알려주시면 감사하겠습니다.
#java #thread #java스레드 #java팁 #java공부 #자바공부 #자바 #java오류 #멀티스레드 #멀티스레드충돌