Java Race Condition

Chunbae·2025년 1월 16일
0
post-thumbnail

Java Race Condition

Java는 "멀티 스레드환경"의 프로그래밍 언어로 여러개의 스레드가 동시 접근이 가능하여 데이터를 변경 가능하기 때문에 Race Condition문제가 발생할 위험이 존재합니다.

Race Condition?

Race Condition이란 "경쟁 상태"라는 의미로 두개 이상의 스레드가 공유 메모리에 접근하는 프로그램의 일부인 "임계 영역(Critical Section)에 접근하여 동시 실행시 발생되는 문제를 말합니다.
멀티스레드 환경에서는 여러 스레드가 동시에 특정 공유자원을 얻기 위해 경쟁하는 상황을 의미하여 이로 인해 런타임 오류나 실행에 문제가 발생할 가능성이 높습니다.



Race Condition의 유형

Read - Modify - Write

여러 스레드가 동일한 변수 값을 읽은 후 수정하고 다시 쓰는 상황에서 발생하는 Race Condition입니다.

public class ReadModifyWriteTest {
	private int counter = 0;
	
	public void increment(){
		int temp = counter; // 읽기
		temp += 1; // 수정
		counter = temp; // 쓰기
	}
	
	public int getCounter(){
		return counter;
	}
	
}

public class Main {
	public static void main(String[] args) throws InterruptedException {
        ReadModifyWriteTest ex = new ReadModifyWriteTest();

        int threadCount = 100; //테스트 스레드
        Thread[] threads = new Thread[threadCount];

        for(int i = 0; i < threadCount; i++) {
            threads[i] = new Thread(ex::increment);
        }

        for (Thread thread : threads) {
            thread.start();
        }

        for (Thread thread : threads) {
            thread.join();
        }

        System.out.println("최종 Counter값 (예상 값 : " + threadCount + " ) : " + ex.getCounter() );
	}
}		
// 최종 Counter값 (예상 값 : 100 ) : 99
counter의 변수가 예기치 않게 덮어쓰여져 예상 결과와 다른 결과가 발생했습니다.

Check - Then - Act

스레드가 조건 확인 후 만족된다는 가정하에 동작을 수행할때 발생하는 Race Condition입니다.

public class CheckThenActExample {
    private boolean flag = false;

    public void checkAndSet() {
        if (!flag) { // 조건 확인
            System.out.println(Thread.currentThread().getName() + ": flag를 설정합니다.");
            flag = true; // 조건이 만족될 때 실행
        } else {
            System.out.println(Thread.currentThread().getName() + ": 이미 설정되었습니다.");
        }
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        CheckThenActTest ex = new CheckThenActTest();

        int threadCount = 100;
        Thread [] threads = new Thread[threadCount];

        for (int i = 0; i < threadCount; i++) {
            threads[i] = new Thread(ex::checkThenAct, "Thread " + i);
        }

        for (Thread thread : threads) {
            thread.start();
        }
        for (Thread thread : threads) {
            thread.join();
        }
    }
}

/*
Thread 0 : flag Setting
Thread 2 : flag Setting
Thread 1 : flag Setting
Thread 3 : flag Setting
Thread 4 : flag Setting
Thread 6: 이미 설정되었습니다.
Thread 5: 이미 설정되었습니다.
Thread 7: 이미 설정되었습니다.
.
.
.
*/



Race Condition 방지방법

방지 방법은 다양하게 있지만 자주 사용되는 방법을 위주로 정리했습니다.

Race Condition은 목적에 따라 사용방법이 달라집니다.
간단 동기화synchronized 를, 조건 변수를 사용해야하면 ReentrantLock자원 최적화Thread Pool 을 주로 사용합니다.

Mutual Exclustion 상호 배제

공유자원에 접근 시 한번에 하나의 스레드만 접근 하도록 제한 하는 기술입니다.
제한을 하면 스레드가 동시에 자원에 접근하여 수정하며 발생하는 문제를 방지 할 수 있습니다.

Synchronized

특정 메서드나 코드 블럭을 동기화하여 해당 영역에 한번에 하나의 스레드만 접근하도록 보장합니다.

  • 사용이 간단함
  • 메서드 또는 코드 블럭 수준에서 적용 가능
  • JVM이 제공하는 모니터락을 사용
public class MutualExclusionTest {
    private int counter = 0;

    public synchronized void increment(){
        counter++;
    }
    public synchronized int getCounter(){
        return counter;
    }
}

public class MutualExclusionMain {

    public static void main(String[] args) throws InterruptedException {
        MutualExclusionTest ex = new MutualExclusionTest();

        int threadCount = 10;
        Thread[] threads = new Thread[threadCount];

        for (int i = 0; i < threadCount; i++) {
            threads[i] = new Thread(ex::increment);
        }
        for (Thread thread : threads) {
            thread.start();
        }
        for (Thread thread : threads) {
            thread.join();
        }
        System.out.println("최종 counter 값: " + ex .getCounter()); //10
    }
}

ReentrantLock

동기화를 위한 세밀한 제어를 제공합니다.

  • 락을 명시적으로 해제해야함.
  • 조건 변수를 활용하여 복잡한 로직 구현 가능
  • 데드락의 위험이 있고 과도한 사용시 성능 저하
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockTest {

    private int counter = 0;
    private final ReentrantLock lock = new ReentrantLock();

    public void increment(){
        lock.lock();
        try{
            counter++;
        }finally {
            lock.unlock();
        }
    }

    public int getCounter() {
        return counter;
    }
}

public class ReentrantLockMain {

    public static void main(String[] args) throws InterruptedException {
        ReentrantLockTest ex = new ReentrantLockTest();

        int threadCount = 10;
        Thread[] threads = new Thread[threadCount];

        for (int i = 0; i < threadCount; i++) {
            threads[i] = new Thread(ex::increment);
        }
        for (Thread thread : threads) {
            thread.start();
        }
        for (Thread thread : threads) {
            thread.join();
        }

        System.out.println("최종 Counter : " + ex.getCounter());

    }
}

Thread Synchronization 스레드 동기화

작업 간 순서를 보장하여 여러 스레드가 공유 자원에 동시 접근시 발생 가능한 충돌을 방지합니다.

wait(), notify(), notifyAll()

  • 스레드가 특정 조건 만족까지 대기하거나 대기중인 스레드를 깨워 사용
  • synchronized블럭 내에서 사용이 가능
public class ThreadSynchronizationTest {
    private boolean flag = false;

    public synchronized void waitForFlag(){
        while(!flag){
            try{
                System.out.println(Thread.currentThread().getName() + " 대기중..");
                wait();
            }catch(InterruptedException e){
                Thread.currentThread().interrupt();
            }
        }

        System.out.println(Thread.currentThread().getName() + " flag 감지 후 실행");

    }

    public synchronized void setFlag(){
        this.flag = true;
        System.out.println("flag 설정");
        notifyAll(); //실행중 모든 스레드 깨우기
    }
}

public class ThreadSynchronizationMain {

    public static void main(String[] args) throws InterruptedException {
        ThreadSynchronizationTest ex = new ThreadSynchronizationTest();

        Thread t1 = new Thread(ex::waitForFlag, "thread 1");
        Thread t2 = new Thread(ex::waitForFlag, "thread 2");

        t1.start();
        t2.start();

        Thread.sleep(2000);
        ex.setFlag();
    }
}
/*
thread 1 대기중..
thread 2 대기중..
flag 설정
thread 1 flag 감지 후 실행
thread 2 flag 감지 후 실행
*/

Thread Pool

제한된 수의 스레드를 미리 생성하고 작업이 들어오면 스레드를 재사용하여 처리하는 방법입니다.

스레드 풀이 존재하지 않으면 작업 시 매번 새롭게 스레드를 생성해야하므로 Race Condition이 발생할 가능성이 높고 시스템 저하를 야기 합니다.

  • ExcutorService 인터페이스를 사용하여 구성합니다.
  • 스레드 생성을 제한하여 시스템 리소스를 효율적으로 활용
  • 컨텍스트 스위칭을 줄여 성능 향상
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolMain {

    public static void main(String[] args) {

        ExecutorService threadPool = Executors.newFixedThreadPool(3);

        for (int i = 0; i <= 10; i++) {
            int taskNumber = i;
            threadPool.submit(() -> {
                System.out.println(Thread.currentThread().getName() + " 실행 중: 작업 " + taskNumber);
                try{
                    Thread.sleep(1000);
                }catch (InterruptedException e){
                    Thread.currentThread().interrupt();
                }
                System.out.println(Thread.currentThread().getName() + " 완료 : 작업 " + taskNumber);
            });
        }

        threadPool.shutdown();
    }
}



Reference

https://velog.io/@gjwjdghk123/RaceCondition?t
https://www.javatpoint.com/race-condition-in-java?t
https://dzone.com/articles/race-condition-vs-data-race?t

profile
말하는 감자

0개의 댓글