[JAVA8 병렬프로그래밍] 원자적 변수 atomic

반응형
728x90
반응형

예제

Counter.java
package org.example.atomic;

public class Counter {
    private int c = 0;

    public void increment() {
        c++;
    }

    public void decrement() {
        c--;
    }

    public int value() {
        return c;
    }
}

위 예제는 멀티 스레드에서 접근하는 요청에 대한 정합성 확보가 어렵다는 문제점이 있다. 이를 해결하기 위해 각 메서드에 synchronized 키워드를 붙여 객체에 록을 걸어보자.

 

SynchronizedCounter.java
package org.example.atomic;

public class SynchronizedCounter {
    private int c = 0;

    public synchronized void increment() {
        c++;
    }

    public synchronized void decrement() {
        c--;
    }

    public synchronized int value() {
        return c;
    }
}

synchronized 키워드는 Blocking을 사용하여 멀티 스레드 환경에서 공유 객체를 동기화하는 키워드다. 

특정 스레드가 해당 Block의 전체에 lock을 걸면 해당 lock에 접근하는 스레드들은 Blocking 상태로 기다리게된다.

기다리는 동안 자원이 낭비되고 성능 저하로 이어지게된다.

 

이 단점을 보완한 방법이 아래에 배울 atomic 변수다.

atomic 변수는 blocking을 사용하는 synchronized 키워드와는 달리 non-blocking 하면서 원자성을 보장하여 동기화 문제를 해결한다.

 

 

java.util.concurrent.atomic

자바의 컨커런트 API에서는 원자적 변수 기능을 제공하는데 이는 멤버 변수 자체에서 데이터 정합성을 제공한다.

해당 패키지의 자바 API 문서를 보면 "하나의 변수 선언으로 멀티 스레드 프로그래밍에서 안정성을 보장하는 캘르스들을 모아 놓은 작은 툴킷"이라고 정의하고있다. 그리고 해당 패키지에는 많은 클래스를 정의해 놓았는데 클래스명의 접두어로 Atomic을 사용하였고 뒤에는 자바의 데이터형과 연관된 이름들을 사용했다.

 

제공하는 기능

1) boolean, int, long, int 배열, long 배열 등의 값이 원자적으로 업데이트될 수 있도록 하는 클래스를 제공한다.

2) 객체의 참조형 변수에 원자성을 제공하기 위한 클래스를 제공한다. 이 클래스들은 제네릭으로 설계되어있다.

3) 정수 혹은 실수형에 대한 누적, 추가를 위한 클래스를 제공한다. 

 

AtomicCounter.java
package org.example.atomic;

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicCounter {
    private AtomicInteger c = new AtomicInteger(0);

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

    public void decrement() {
        c.decrementAndGet();
    }

    public int value() {
        return c.get();
    }
}

 

int 변수를 AtomicInteger 클래스로 대체하여 선언하였다. 개발자는 로컬 변수에 대한 동기화를 신경쓰지 않아도 되고, 특히 synchronized 키워드를 사용하지 않고 자바에서 기본 제공하는 원자적 변수를 이용해서 값을 처리할 수 있다.

 

 

atomic의 동작 원리

atomic은 CAS(Compare And Swap) 알고리즘을 사용한다.

CAS 알고리즘 동작 원리
1 인자로 기존 값(Compared Value)과 변경할 값(Exchanged Value)을 전달한다.
2 기존 값(Compared Value)이 현재 메모리가 가지고 있는 값(Destination)과 같다면 변경할 값(Exchanged Value)을 반영하며 true를 반환한다.
3 반대로 기존 값(Compared Value)이 현재 메모리가 가지고 있는 값(Destination)과 다르다면 값을 반영하지 않고 false를 반환한다.
- false를 반환한 경우에는 무한 루프를 구성하여 다시 변경된 값(다른 스레드에 의해 변경된 메모리 값)을 읽고 같은 시도를 반복하거나, 다른 더 중요한 작업을 수행한다. 이는 개발자가 결정하는 부분이다.

▶ 기존값(Compared Value) != 현재 메모리가 가지고 있는 값

예시) 스레드 A가 공유 변수를 계산하고 메모리에 반영하기 직전에 다른 스레드 B가 공유 변수를 변경하여 메모리에 반영한 경우

 

https://javaplant.tistory.com/23

멀티 쓰레드 환경에서 각 CPU는 메인 메모리에서 변수 값을 참조하는게 아닌, 각 CPU의 캐시 영역에서 메모리 값을 참조하게된다. 

이때, 메인 메모리에 저장된 값과 CPU 캐시에 저장된 값이 다른 경우를 '가시성 문제'라고 하고, 이때 false 를 리턴한다.

 

▶ synchronized 사용의 경우

synchronized 블록 전/후에 메인 메모리와 CPU 캐시 메모리 값을 동기화하여 문제가 없도록 한다.

 

▶ JAVA 에서 CAS 알고리즘 동작

https://highright96.tistory.com/106

상황

JVM 내의 스레드 스케줄러에 의해 각각의 core에 스레드-1과 스레드-2가 선점된 상태다.

두 스레드는 각각 for문 안에서 count를 증가시키고있다. 

 

  • 각 스레드는 heap 영역 내에서 count 변수를 읽어서 CPU Cache Memory에 저장한다.
  • 각 스레드는 번갈아가며 for문을 돌며 count 값을 1씩 증가시킨다.
  • 스레드-1 또는 스레드-2는 변경한 count 값을 heap 영역에 반영하기 위해, 변경하기 전의 count 값과 heap에 저장된 count 값을 비교한다.
    • 변경하기 전의 count 값과 heap에 저장된 count 값이 다를 경우 false를 반환한다. 여기서 heap 영역의 값을 다시 읽어들여, 위 2)번으로 다시 돌아간다.
    • 변경하기 전의 count 값과 heap에 저장된 count 값이 같은 경우 heap 영역에 변경한 값을 저장한다.
  • 힙에 변경한 값을 저장한 스레드-1 또는 스레드-2는 1)번의 과정으로 돌아가서 for문을 마칠때까지 반복 수행한다.

 

▶ 다시 위 예제를 보자.

AtomicCounter.java
package org.example.atomic;

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicCounter {
    private AtomicInteger c = new AtomicInteger(0);

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

    public void decrement() {
        c.decrementAndGet();
    }

    public int value() {
        return c.get();
    }
}

incrementAndGet() 메서드 내부에서 위에서 설명한 'CAS 알고리즘'의 로직을 구현하고 있다.

 

  • AtomicInteger.java > incrementAndGet()
public final int incrementAndGet() {
    return U.getAndAddInt(this, VALUE, 1) + 1;
}

 

  • getAndAddInt()
@HotSpotIntrinsicCandidate
public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);
    } while (!weakCompareAndSetInt(o, offset, v, v + delta));
    return v;
}

weakCompareAndSetInt() 결과를 리턴받아서 성공할때까지 while 문으로 반복 수행한다.

weakCompareAndSetInt() 내부에서 호출되는 메서드에서 메모리에 저장되어진 값과 현재 CPU 캐시에 저장된 값을 비교하여 값을 리턴하고, 이 결과 값으로 true가 될때까지 while문이 수행되는 로직이다.

 

 

 

♧ Reference

https://javaplant.tistory.com/23

 

 

반응형

Designed by JB FACTORY