우선, Atomicity(원자성)의 개념을 알아야 한다.
Atom = 더 이상 쪼갤 수 없는 단위
처음부터 끝까지 실행이 되거나 아예 아무것도 실행되지 않던가 하는 Action이다.
쇼핑몰에서의 '결제' -> '상품 재고 수량 변경' 은 하나만 이뤄져서는 안되고 둘 다 실행되거나 둘 다 실행되지 않아야 한다.
결제가 되었다면 재고에서 빼야 나중에 재고가 없는데 주문을 받지 않을 수 있다.
"중간에 멈춰선 안되는 연산의 중요성"?
여러 개의 작업을 쪼개서 번갈아가면서 실행하는 멀티 스레드 환경에서 비 원자 연산이 돌아가면 위와 같은 문제가 생길 수 있다.
작업 단위가 분리되면 안되는 연산 = Atomic operation이 필요하다.
멀티스레드 환경에서 동시성 문제를 제어할 수 있도록 java는 여러 형태로 Atomic Operation을 지원한다.
대표적으로 volatile, synchronization, Atomic 3가지가 있다.
Atomic Type의 경우, 예약어에 해당하는 앞의 두개와 다르게 java.util.concurrent.atomic 패키지에 정의된 클래스이다.
class MyLock {
private boolean locked = false;
public boolean tryLock() {
if (!locked) {
locked = true;
return true;
}
return false;
}
}
MyLock 클래스에는 여러 개의 스레드가 lock을 얻기 위해 경쟁할 때 lock의 상태 관리하는 로직이 정의되어 있다.
위의 코드에서 개발자가 의도한 동작은 여러개의 스레드 중 오직 하나의 스레드만 락을 얻게 되는 것이다.
class TestRunnable implements Runnable {
private final MyLock myLock;
public TestRunnable(MyLock myLock) {
this.myLock = myLock;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " - " + myLock.tryLock());
}
}
실행하는 순간 lock획득을 시도하는 Runnable의 구현체를 정의한다.
public class Main {
public static void main(String[] args) {
final MyLock myLock = new MyLock(); // shared resource
for (int i = 0; i < 100_00; i++) {
new Thread(new TestRunnable(myLock)).start();
}
}
}
main에선 하나의 Lock 객체를 공유하는 10만개의 스레드를 생성하고 실행한다.
위에서 작성한 tryLock의 원자성이 보장되어야 한다.
변수 선언 시, 타입을 boolean에서 AtomicBoolean으로 바꿔보자.
locked가 false일 때만 값을 true로 변경하고 이미 값이 true라면 set은 생략한다.
compareAndSet 내부에서 값을 원자적으로 갱싱하기에 동시성 문제가 해결된다.
아래와 같이 바꿔보자.
class MyLock {
private AtomicBoolean locked = new AtomicBoolean();
public boolean tryLock() {
if (!locked.get()) {
// 비용이 큰 작업을 수행한다
for (int i = 0; i < 100_000; i++) { }
}
return locked.compareAndSet(false, true);
}
}
위에서 잠깐 언급한 Atomic type이 synchronized를 쓰지 않고도 동시성을 유지하는 CAS 알고리즘을 간단하게 정리하자면
Compare-And-Set이란 이름처럼 값을 넣었을 때, 해당 값과 메모리 상의 값이 일치하는 지 비교해서 다르다면 중간에 다른 스레드가 들어온 것으로 판단하여 write를 실패시킨다.
내부적으론 do-while문으로 되어 있어서 맨 처음 값만 write가 가능하고 이후는 체킹을 반복하게 된다.