티스토리 뷰

Java/Java

<Java> 원자적 연산이란?

면목동인간 2025. 8. 9. 15:21

원자적 연산이란?

 원자적 연산이란 연산이 더 이상 나눌 수 없는 단위로 수행된 것을 의미한다. 즉 멀티스레드 상황에서 다른 스레드의 간섭 없이 안전하게 처리되는 연산이다.

// 원자적 연산인 경우
int i = 0;

// 원자적 연산이 아닌 경우
i = i + 1;
i++;

 위의 연산을 보면 i = 0은 둘로 쪼갤 수 없는 원자적 연산이다. 그에 반해 i = i + 1과 i++은 원자적 연산이 아니다. 왜냐하면 이 연산은 아래와 같이 나누어 실행되기 때문이다.

  1. i의 값을 읽는다. i의 값은 0이다.
  2. 읽은 0에 1을 더해서 1을 만든다.
  3. 더한 1을 i 변수에 대입한다.

 원자적 연산이 아닌 경우 멀티스레드 환경에서 안전하게 연산을 수행할려면 임계 영역을 만들어야 할 경우 synchronized 블록이나 Lock 등을 사용해서 만들어야 한다.

 

원자적 연산 예제 1

public interface IncrementInteger {
    void increment();

    int get();
}
public class BasicInteger implements IncrementInteger {

    private int value;

    @Override
    public void increment() {
        value++;
    }

    @Override
    public int get() {
        return value;
    }
}
public class VolatileInteger implements IncrementInteger {

    volatile private int value;

    @Override
    public void increment() {
        value++;
    }

    @Override
    public int get() {
        return value;
    }
}
public class SyncInteger implements IncrementInteger {

    private int value;

    @Override
    public synchronized void increment() {
        value++;
    }

    @Override
    public synchronized int get() {
        return value;
    }
}
public class MyAtomicInteger implements IncrementInteger {

    AtomicInteger atomicInteger = new AtomicInteger(0);

    @Override
    public void increment() {
        atomicInteger.incrementAndGet();
    }

    @Override
    public int get() {
        return atomicInteger.get();
    }
}
public class IncrementThreadMain {

    public static final int THREAD_COUNT = 1000;

    public static void main(String[] args) throws InterruptedException {
        test(new BasicInteger());
        test(new VolatileInteger());
        test(new SyncInteger());
        test(new MyAtomicInteger());
    }

    private static void test(IncrementInteger incrementInteger) throws InterruptedException {

        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                sleep(10); // 너무 빨리 실행되기 때문에, 다른 스레드와 동시 실행을 위해 잠깐 쉬었다가 실행
                incrementInteger.increment();
            }
        };
        List<Thread> threads = new ArrayList<>();
        for (int i = 0; i < THREAD_COUNT; i++) {
            Thread thread = new Thread(runnable);
            threads.add(thread);
            thread.start();
        }

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

        int result = incrementInteger.get();
        System.out.println(incrementInteger.getClass().getSimpleName() + " result: " + result);
    }
}

 위의 코드에서 연산을 실행하기 위해 4개의 구현 객체를 생성했다. BasicInteger은 CPU 캐시를 사용하는 일반적인 연산이고, VolatileInteger은 연산을 메인 메모리로 사용하도록 하는 연산이고, SyncInteger은 안전한 임계 영역을 만든 연산이고, MyAtomicInteger은 멀티스레드 상황에서 안전하게 연산을 수행할 수 있는 AtomicInteger 클래스를 사용한 연산이다.

 

실행 결과

BasicInteger result: 993
VolatileInteger result: 994
SyncInteger result: 1000
MyAtomicInteger result: 1000

 위의 실행 결과를 보면 BasicInteger,VolatileInteger 연산 클래스는 원자적이지 않은 연산이다. 왜냐하면 여러 스레드가 동시에 같은 value의 값을 읽고 연산을 수행하기 때문이다. 그런데 SyncInteger과 MyAtomicInteger은 원자적이지 않은 연산이지만 안전한 임계영역을 통한 멀티스레드에서 안전하게 사용할 수 있는 연산이라 정상적으로 증가 연산을 수행한 것이다.

 

원자적 연산 예제2

public class IncrementPerformanceMain {

    public static final long COUNT = 100_000_000;

    public static void main(String[] args) {
        test(new BasicInteger());
        test(new VolatileInteger());
        test(new SyncInteger());
        test(new MyAtomicInteger());
    }

    private static void test(IncrementInteger incrementInteger) {
        long startMs = System.currentTimeMillis();

        for (long i = 0; i < COUNT; i++) {
            incrementInteger.increment();
        }

        long endMs = System.currentTimeMillis();
        System.out.println(incrementInteger.getClass().getSimpleName() + ": ms=" + (endMs - startMs));
    }
}

 위의 예제는 1억 번의 연산을 수행하여 4개의 연산 클래스의 성능(시간)을 테스트하였다.

 

실행 결과

BasicInteger: ms=196
VolatileInteger: ms=588
SyncInteger: ms=1978
MyAtomicInteger: ms=674
  1. BasicInteger은 CPU 캐시를 적극 사용하고 임계 영역도 없기 때문에 가장 빠르다. 하지만 멀티스레드 상황에는 적합하지 않다. 
  2. VolatileInteger은 CPU 캐시를 사용하지 않고 메인 메모리를 사용하기 때문에 BasicInteger 보다 좀 느리고 마찬가지로 멀티스레드 상황에 안전하지 않다.
  3. SyncInteger은 안전한 임계 영역이 있기 때문에 락을 획득하고 반납하는 시간 등으로 인해 속도가 느리지만 멀티스레드 환경에는 안전하다.
  4. MyAtomicInteger은 자바가 제공하는 AtomicInteger을 사용하고 멀티스레드 상황에 안전하게 사용하고 성능도 SyncInteger 보다 약 2배 정도 빠르며 멀티스레드 환경에 안전하다.

 

정리

 원자적 연산은 연산을 더 이상 나눌 수 없는 단위로 수행하는 것을 의미하며, 그렇지 않은 연산은 멀티스레드 환경에 안전하지 않은 경우가 있다. 그럴 경우 안전한 임계영역을 만들어 연산을 수행하면 되고, 특히 자바가 제공하는 AtomicInteger을 사용하여 안전하고 빠른 연산을 수행할 수 있다.


본 포스팅은 “김영한의 실전 자바 - 고급 1편, 멀티스레드와 동시성/인프런”를 학습한 내용을 정리한 것

'Java > Java' 카테고리의 다른 글

<Java> yield() 메서드란?  (0) 2025.05.10
<Java> 버퍼 이란?  (0) 2025.03.04
<Java> Object 객체의 wait(), notify(), notifyAll() 메서드란?  (1) 2025.01.25
<Java> ReentrantLock이란?  (0) 2024.10.26
<Java> sychronized이란?  (1) 2024.10.22
댓글
최근에 올라온 글
«   2026/03   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함
Total
Today
Yesterday