컴퓨터 과학에서 사용하는 우너자적 연산의 의미는 해당 연산이 더 이상 나눌 수 없는 단위로 수행된다는 것을 의미한다. 즉 원자적 연산을 중단되지않고, 다른 연산과 간섭없이 완전히 실행되거나 전혀 실행되지 않는 성질을 가지고 있다. 쉽게 얘기해서 멀티스레드 상황에서 다른 스레드의 간섭 없이 안전하게 처리되는 연산이라는 의미이다.
예를 들어서 volatile int i =0
와 같은 식이 있을 때 i의 오른쪽 값을 i에 대입하는 식 1개뿐이므로 둘로 쪼갤수 없는 원자적 연산이다.
하지만 i+=1
은 원자적 연산이 아니다. i의 값 읽기, i에 1더하기 , 그 값을 다시 i에 대입하기로 식이 나눌수 있기 때문이다. 코드 예시로 원자적이지 않은 식을 멀티 스레드에서 썻을때 어떤 문제가 생기는지 알아보자.
public class BasicInteger implements IncrementInteger {
private int value;
@Override
public void increment() {
value++;
}
@Override
public int get() {
return value;
}
}
value
값은 인스턴스의 필드이기 때문에 여러 스레드가 공유할 수 있다. 이렇게 공유 가능한 자원에 ++
같은 원자적이지 않은 연산을 사용하면 문제가 생길수 있다.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);
}
}
상수 만큼의 스레드를 생성하고 increment()
메서드를 호출
스레드를 1000개 생성했다면 메서드도 1000번 호출하기 때문에 결과는 1000이 되어야 하지만 실행하면 1000이 아닌 다른 숫자가 나온다
이렇게 연산자체가 나눠진 경우에는 synchronized
블럭이나 Lock
등을 사용해서 안전한 임계 영역을 만들어야한다.
원자적 연산 - AtomicInteger
AtomicInteger
를 사용하면 멀티스레드 상황에서 안전하게 증가 연산을 수행할 수 있다.
public class MyAtomicInteger implements IncrementInteger {
AtomicInteger atomicInteger = new AtomicInteger(0);
@Override
public void increment() {
atomicInteger.incrementAndGet();
}
@Override
public int get() {
return atomicInteger.get();
}
}
new AtomicInteger(0):
초기값을 지정한다. 생략하면 0부터 시작한다.incrementAndGet():
값을 하나 증가하고 증가된 결과를 반환한다get():
현재값을 반환한다.