TIPS

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

Hacs 2025. 4. 28. 09:00
728x90
반응형
SMALL

 

 

멀티스레드 환경에서는

여러 개의 스레드가 동시에 데이터를 변경할 때 경합 조건(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오류 #멀티스레드 #멀티스레드충돌

 

728x90
반응형
LIST