원자적 연산은, 연산이 더 이상 나눌 수 없는 단위로 수행된 다는 걸 의미.
int i = 0;
이라는 코드가 있다.
해당 코드는 연산
측면에서는 더 이상 나눌 수 없는 원자적 연산
이라고 표현 할 수 있다.
i = i + 1;
해당 코드는 원자적 연산
일까?
정답은, 아니다 이다.
해당 코드는 두가지 작업으로 나눌 수 있다.
i
의 값을 읽고 (step1),
읽어온 i
의 값에 1을 더한다로 표현 할 수 있다.
i++
또한, i = i + 1;
의 코드를 다르게 표현 한 것일 뿐, 같은 코드이다.
package cas;
public interface IncrementInteger {
void increment();
int get();
}
package cas;
public class BasicInteger implements IncrementInteger{
private int value;
@Override
public void increment() {
value++;
}
@Override
public int get() {
return value;
}
}
package cas;
import java.util.*;
public class IntegerTestMain {
public static final int THREAD_COUNT = 1000;
public static void main(String[] args) throws InterruptedException {
test(new BasicInteger());
}
private static void test(IncrementInteger incrementInteger)
throws InterruptedException {
Runnable runnable = () -> {
try {
Thread.sleep(10);
incrementInteger.increment();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
};
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);
}
}
//결과값
value = 989
989
이 나왔다.incrementInteger.increment();
로 해석 할 수 있다.공유 자원
인 value
에 접근하여 ++
연산을 시도 하는데, 이는 원자적 연산
은 아니다.값을 불러오고
, 증가를 시킨다
두가지 Step 으로 진행하게 되는데 이때, 중간에 다른 스레드에서 증가, 저장을 시키면 데이터의 정합성 문제가 발생된다.synchronized
키워드를 사용해서 임계영역 코드
를 안전하게 보호 하면 된다.synchronized
키워드를 사용해서 한번에 하나의 스레드만 접근하게 처리하면 앞서 다룬 분석의 문제점이 해결된다.package cas;
public class BasicIntegerSynchronized implements IncrementInteger{
private int value;
@Override
public synchronized void increment() {
value++;
}
@Override
public synchronized int get() {
return value;
}
}
BasicIntegerSynchronizedresult: 1000
Synchronized
를 통해 해결 하는 방법 말고도, 원자적 연산을 지원하는 클래스를 지원한다.AtomicInteger
, Atomic~~~
등이 있다.AtomicInteger
를 사용하여 문제를 회피 해보겠다.package cas;
import java.util.concurrent.atomic.AtomicInteger;
public class BasicIntegerAtomicInteger implements IncrementInteger {
private AtomicInteger value = new AtomicInteger(0);
@Override
public void increment() {
value.incrementAndGet();
}
@Override
public int get() {
return value.get();
}
}
BasicIntegerresult: 991
BasicIntegerSynchronizedresult: 1000
BasicIntegerAtomicIntegerresult: 1000
package cas;
public class PerformanceTest {
public static final int LOOP_COUNT = 100_000_000;
public static void main(String[] args) {
test(new BasicInteger());
test(new BasicIntegerSynchronized());
test(new BasicIntegerAtomicInteger());
}
private static void test(IncrementInteger incrementInteger){
long startTime = System.currentTimeMillis();
for(int i=0; i<LOOP_COUNT; i++) {
incrementInteger.increment();
}
long endTime = System.currentTimeMillis();
System.out.println(incrementInteger.getClass().getSimpleName() + " , Running Time = " + (endTime - startTime) + "ms");
}
}
BasicInteger , Running Time = 8ms
BasicIntegerSynchronized , Running Time = 1684ms
BasicIntegerAtomicInteger , Running Time = 593ms
BasicInteger
가 가장 빠른 성능을 보여주었다.AtomicInteger
가 Synchronized
보다 빠르게 동작 한 것이다.Synchronized
는 임계 영역의 코드를 실행하기 위해 Lock
을 사용하여 순차적인 접근으로 데이터의 정합성을 보장하였다.CAS
을 통해서 이 문제를 해결하였다.Lock
전략을 사용하는 것은 해결 방법에 있어 좋은 방법이지만 몇가지 문제점이 있다.CAS
연산은 Lock
을 사용하지 않고, 원자적 연산을 수행 할 수 있다.CAS는 다음과 같은 순서로 동작합니다:
현재 값 읽기
: 메모리 위치에 저장된 현재 값을 읽습니다.값 비교
: 읽어온 현재 값이 기대 값과 일치하는지 비교합니다.값 교환
: 만약 현재 값이 기대 값과 일치한다면, 새로운 값으로 메모리 위치의 값을 원자적으로 교환합니다. 만약 일치하지 않는다면, 교환은 이루어지지 않고, 메모리 위치의 값은 그대로 유지됩니다.실패 CASE 상세
: 실패 한다면, 다른 스레드가 값을 중간에 변경한 것이므로, 다시 처음으로 돌아가 위 과정을 반복한다.실패 CASE
가 증가 할 수록, 즉 충돌이 많이 날 수록 성능이 나빠진다.Java
에서는 CAS 연산을 통해 동시성 제어를 시도한다.각각의 특징은 다음과 같다.
락(Lock) 방식
CAS(Compare-And-Swap) 방식
CAS의 장점
CAS의 단점
동기화 락의 장점
동기화 락의 단점
즉, 간단한 연산이 주로 이뤄, 충돌이 날 가능성이 적은 경우, (하지만 발생 가능성이 1% 라도 있다면) CAS
선택
다른 스레드들 간에 충돌이 자주 발생할 것으로 예상이 되면, Lock 전략을 선택하는 것을 추천한다.
사실 대부분의 실무 상황에서는 동기화 락이 효율적인 가능성이 높다.