TIL(Today I Learned)

99클럽 코테 스터디 9일차 TIL + 동시성이슈

zincah 2025. 4. 10. 22:00
반응형

 

오늘의 키워드

  • 동시성 이슈 (Concurrency Issue)

 

동시성 이슈란?

 

여러 개의 스레드 또는 프로세스가 공유 자원 (Shared Resource)에 동시에 접근하려고 할 때 발생하는 예기치 않은 문제들을 의미합니다. 공유 자원은 메모리, 파일, 데이터베이스 연결 등이 될 수 있습니다.

 

동시성 이슈 발생 이유 

여러 스레드가 동시에 실행될 때, 각 스레드의 실행 순서는 운영체제의 스케줄링에 따라 예측하기 어렵습니다. 따라서 공유 자원에 대한 접근 순서가 매번 달라질 수 있으며, 이로 인해 프로그램의 결과가 예상과 다르게 나타나는 상황이 발생할 수 있습니다.

 

동시성 이슈 해결 방법: 동기화 (Synchronization)

동시성 이슈를 해결하는 가장 일반적인 방법은 동기화 (Synchronization) 메커니즘을 사용하여 공유 자원에 대한 접근을 제어하는 것입니다. 이를 통해 여러 스레드가 동시에 공유 자원을 수정하는 것을 방지하고, 데이터의 일관성을 유지할 수 있습니다.

 

 

예제

class Counter {
    private int count = 0;

    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

public class ConcurrencyExample {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Runnable incrementTask = () -> {
            for (int i = 0; i < 100000; i++) {
                counter.increment();
            }
        };

        Thread thread1 = new Thread(incrementTask);
        Thread thread2 = new Thread(incrementTask);

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println("최종 카운터 값: " + counter.getCount());
    }
}

 

위 코드에서 예상하는 결과는 200000일테지만 실행할 때마다 다른 값이 나올 수 있습니다.

예를 들어 두 스레드가 동시에 count 변수의 값을 읽어들인 뒤 각각 1을 더하고 저장하게되면 증가가 1번만 반영되는 결과가 발생할 수 있습니다. 

 

해결방법1 : synchronized 키워드를 사용

synchronized를 사용해서 메서드 또는 특정 코드 블록에 대한 접근을 동기화할 수 있습니다.

 

class SynchronizedCounter {
    private int count = 0;

    // 메서드 전체를 동기화
    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

public class SynchronizedConcurrencyExample {
    public static void main(String[] args) throws InterruptedException {
        SynchronizedCounter counter = new SynchronizedCounter();

        Runnable incrementTask = () -> {
            for (int i = 0; i < 100000; i++) {
                counter.increment();
            }
        };

        Thread thread1 = new Thread(incrementTask);
        Thread thread2 = new Thread(incrementTask);

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println("최종 카운터 값: " + counter.getCount());
    }
}

 

  • synchronized 메서드는 해당 객체에 대한 내부 잠금 (Intrinsic Lock 또는 Monitor Lock)을 획득한 스레드만 실행할 수 있습니다. 다른 스레드는 잠금이 해제될 때까지 대기합니다.
  • 이렇게 함으로써 여러 스레드가 동시에 increment() 메서드를 실행하여 count 변수를 수정하는 것을 방지하고, 연산이 원자적으로 수행되도록 보장합니다.

 

 

해결방법2 : AtomicInteger 사용

java.util.concurrent.atomic 패키지는 원자적으로 연산을 수행할 수 있는 클래스들을 제공합니다.

AtomicInteger를 사용하여 동기화 없이 스레드 안전한 count 변수를 구현할 수 있습니다.

 

import java.util.concurrent.atomic.AtomicInteger;

class AtomicCounter {
    private AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet();
    }

    public int getCount() {
        return count.get();
    }
}

public class AtomicConcurrencyExample {
    public static void main(String[] args) throws InterruptedException {
        AtomicCounter counter = new AtomicCounter();

        Runnable incrementTask = () -> {
            for (int i = 0; i < 100000; i++) {
                counter.increment();
            }
        };

        Thread thread1 = new Thread(incrementTask);
        Thread thread2 = new Thread(incrementTask);

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println("최종 카운터 값: " + counter.getCount());
    }
}

 

 

  • AtomicInteger 객체를 사용하여 카운터를 관리합니다.
  • incrementAndGet() 메서드는 값을 원자적으로 증가시키고 증가된 값을 반환합니다. 이는 스레드 안전하게 수행됩니다.
  • AtomicInteger를 사용하면 명시적인 잠금 없이도 동시성 문제를 해결할 수 있으며, 일반적으로 synchronized 키워드를 사용하는 것보다 성능이 좋습니다.

 

그 외 Java에서 사용하는 동기화 메커니즘은 아래와 같습니다.

  • synchronized 키워드: 메서드 또는 코드 블록을 동기화합니다.
  • java.util.concurrent.locks.Lock 인터페이스 및 구현체 (ReentrantLock 등): 더 세밀한 잠금 제어를 제공합니다.
  • java.util.concurrent.Semaphore: 접근 가능한 자원의 수를 제어합니다.
  • java.util.concurrent.CountDownLatch: 하나 이상의 스레드가 특정 작업이 완료될 때까지 기다리도록 합니다.
  • java.util.concurrent.CyclicBarrier: 정해진 수의 스레드가 모두 특정 지점에 도달할 때까지 기다리도록 합니다.
  • java.util.concurrent.atomic 패키지: 원자적 연산을 지원하는 클래스를 제공합니다.
  • java.util.concurrent 컬렉션: 스레드 안전한 컬렉션 클래스를 제공합니다 (ConcurrentHashMap, CopyOnWriteArrayList 등).

 

오늘의 회고

동시성 이슈, 항해99에서 진행하는 또 다른 코스에서 제시해준 면접 질문 중 하나였습니다. 현재 회사에서 다루고 있는 솔루션도 동시성 이슈에 대해 많은 고민을 하게되다보니 알고는 있었지만 글로 정리해보니 모르는 부분도 많았습니다. 

Java에서 사용하는 동기화 메커니즘 중에서는 모르는 부분도 많아서 더 공부를 해보려합니다.

 

반응형