- 현재 쓰레드에 저장된 값과 메인 메모리에 저장된 값을 비교해서 일치하는 경우, 새로운 값으로 교체하고, 일치하지 않으면 재시도 진행
synchronized
또는 lock
을 사용하지 않고 동기화 문제(race condition)을 해결할 수 있다.바꾸려는 변수
바꾸려는 변수의 기댓값
업데이트 할 값
package java.util.concurrent.atomic;
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value");
private volatile int value;
...
public final boolean compareAndSet(int expectedValue, int newValue) {
return U.compareAndSetInt(this, VALUE, expectedValue, newValue);
}
}
package jdk.internal.misc;
public final class Unsafe {
...
@IntrinsicCandidate
public final native boolean compareAndSetInt(Object o, long offset,
int expected,
int x);
}
AtomicInteger
를 보면 value
가 volatile
로 선언된 것을 볼 수 있다.
value
의 값을 가져올 때는 바로 메인 메모리에서 읽어오고, 수정하게 되어도 메인 메모리에 바로 수정한다Object o
(AtomicInteger
)의 값이 expected
와 같다면 그 값을 x
로 업데이트 해준다
compareAndSetInt
의 과정을 atomic하게 처리해준다public final native boolean compareAndSetInt(Object o, long offset, int expected, int x);
이 메서드는 native
메서드이기에 JVM 내부 구현에 의해 직접 기계어 수준에서 실행된다는 의미다.
그래서 이 메서드를 사용하면 하드웨어적으로 해당 메서드의 원자성을 보장해준다.
import java.util.concurrent.atomic.AtomicInteger;
public class CASExample {
private static AtomicInteger counter = new AtomicInteger(0);
public static void incrementCounter() {
// 현재 값 가져오기
int currentValue;
int newValue;
do {
currentValue = counter.get(); // 현재 카운터 값 읽기
newValue = currentValue + 1; // 새 값 계산
} while (!counter.compareAndSet(currentValue, newValue));
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
incrementCounter();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
incrementCounter();
}
});
t1.start();
t2.start();
// 메인 스레드가 두 작업 스레드의 종료를 기다립니다.
t1.join();
t2.join();
// 최종 카운터 값 출력
System.out.println("Final counter value: " + counter.get());
}
}
new AtomicInteger(0);
인자로 넘어가는 값 0은 내부적으로 volatile
변수에 할당된다
이후에 .get()
을 통해 값을 가져올 때, CPU Cache가 아닌 메인 메모리에서 값을 읽어온다
모든 스레드가 해당 값의 변경사항을 즉각적으로 볼 수 있음
매 반복마다 메인 메모리에서 현재 값을 가져와서 (currentValue = counter.get();
)
업데이트 할 값 설정하고 (newValue = currentValue + 1;
)
가져온 값(currentValue
)과 메인 메모리에 저장되어 있는 값(counter
객체 안에 저장되어 있는 값)을 비교해 같으면 newValue
로 메모리에 저장되어 있는 값을 수정하고 while문을 빠져 나간다
만약 두 값이 다르다면 counter.compareAndSet(currentValue, newValue)
의 결과는 false
과 되어 결국 while문을 빠져나가지 못하고 다시 반복된다.
결국, 이 루프는 currentValue
와 counter
의 값이 같아서 compareAndSet
을 실행할 때까지 계속 반복된다
t1 스레드와 t2 스레드가 do-while문을 통해 계속 compareAndSet
메서드를 성공시키려고 도전한다
결국, 둘 중 누군가 하나는 더 빨리 compareAndSet
메서드를 먼저 수행해서 counter
의 값과 currentValue
값을 맞춘다면 counter
의 값은 newValue
로 업데이트 된다
직후에 들어온 스레드 입장에서는 counter
의 값은 이미 1 증가되었고 내 currentValue
값과 다르기에 실패하고 다시 do-while문을 실행한다
compareAndSet
(정확히 말하면 compareAndSetInt
)을 원자적으로 실행되는 것을 보장해주기에 lock
이 필요 없음synchronized
와 다르게 blocking이 아닌 nonblocking하면서 동기화 문제를 해결non blocking이기에 CAS에서 while의 조건문에서 false
를 받아도 thread가 waiting pool에 들어가지 않고, 계속 resource 얻기 위해 시도할 수 있다(무한 loop)
false
받았을 때, 다른 방법을 취할지 결정할 수 있다즉, 해당 resource 얻는 것을 실패했더라도 대기 상태로 들어가지 않는다
blocking은 특정 조건이 충족될 때까지 “대기 상태”로 전환되어 스케쥴링되지 않는다
busy waiting은 nonblocking 동작인데, 특정 조건이 충족될 때까지 계속 loop를 돌면서 조건을 체크한다
결론 : 특정 조건을 충족될 때까지 멈추고 있냐(blocking), 멈추지 않고 계속 동작하냐(busy waiting)의 차이다