Java에서 Synchronized는 비관적 잠금의 대표적인 예로, 어떤 데이터 접근이든 무조건 잠금을 걸어야 한다는 원칙을 따릅니다. 이는 사용자 모드와 커널 모드 간 전환, 잠금 카운터 관리 및 차단된 스레드 검사 등을 포함하는 복잡한 과정을 필요로 합니다.
하지만 하드웨어 명령어 집합의 발전으로 인해 충돌 감지 기반의 낙관적 동시성 제어가 가능해졌습니다. 먼저 작업을 수행하고, 만약 다른 스레드가 데이터를 수정하지 않았다면 작업은 성공적으로 완료됩니다. 만약 충돌이 발생하면 추가적인 보상 조치를 취합니다.
낙관적 동시성 제어의 많은 구현은 스레드를 중단시키지 않으므로 비차단 동기화라고도 불립니다.
낙관적 잠금의 핵심 알고리즘은 CAS(Compare and Swap, 비교 및 교환)입니다. 이 알고리즘은 메모리 값, 예상 값, 새 값의 세 가지 연산 수를 포함합니다.
예상 값과 메모리 값이 같을 때만 메모리 값을 새 값으로 변경합니다.
이는 특정 메모리 블록의 값이 읽은 후에도 변하지 않았는지 확인한 다음, 변했다면 다른 스레드가 이미 수정했음을 의미하며 해당 작업을 폐기하고 그렇지 않으면 새로운 값을 설정하는 논리를 따릅니다.
CAS는 원자성을 가지며, 이는 CPU 하드웨어 명령어에 의해 보장됩니다. 즉, JNI를 통해 네이티브 메서드를 호출하여 C++로 작성된 하드웨어 수준 명령어를 실행하며, JDK에서는 Unsafe 클래스를 제공하여 이러한 작업을 수행할 수 있습니다.
하지만 하드웨어 명령어 집합의 발전으로 인해 충돌 감지 기반의 낙관적 동시성 제어가 가능해졌습니다. 먼저 작업을 수행하고, 만약 다른 스레드가 데이터를 수정하지 않았다면 작업은 성공적으로 완료됩니다. 만약 충돌이 발생하면 추가적인 보상 조치를 취합니다.
낙관적 동시성 제어의 많은 구현은 스레드를 중단시키지 않으므로 비차단 동기화라고도 불립니다.
낙관적 잠금의 핵심 알고리즘은 CAS(Compare and Swap, 비교 및 교환)입니다. 이 알고리즘은 메모리 값, 예상 값, 새 값의 세 가지 연산 수를 포함합니다.
예상 값과 메모리 값이 같을 때만 메모리 값을 새 값으로 변경합니다.
이는 특정 메모리 블록의 값이 읽은 후에도 변하지 않았는지 확인한 다음, 변했다면 다른 스레드가 이미 수정했음을 의미하며 해당 작업을 폐기하고 그렇지 않으면 새로운 값을 설정하는 논리를 따릅니다.
CAS는 원자성을 가지며, 이는 CPU 하드웨어 명령어에 의해 보장됩니다. 즉, JNI를 통해 네이티브 메서드를 호출하여 C++로 작성된 하드웨어 수준 명령어를 실행하며, JDK에서는 Unsafe 클래스를 제공하여 이러한 작업을 수행할 수 있습니다.
Java에서 CAS를 이용한 코드 예제
import java.util.concurrent.atomic.AtomicInteger;
public class CounterManager {
private AtomicInteger counter = new AtomicInteger(0);
public void increaseCounter() {
int currentVal;
int newVal;
do {
currentVal = counter.get();
newVal = currentVal + 1;
} while (!counter.compareAndSet(currentVal, newVal));
}
public int getCounterValue() {
return counter.get();
}
}
위의 예제에서는 `AtomicInteger`를 사용하여 카운터를 관리합니다. `increaseCounter()` 메소드는 CAS를 이용하여 원자적으로 증가 연산을 수행합니다. 현재 값을 가져오고 새 값을 계산한 다음 `compareAndSet()` 메소드를 통해 비교 및 교환을 시도합니다. 만약 현재 값이 예상 값과 일치하면 새 값을 설정하고, 그렇지 않으면 다시 시도합니다.이렇게 함으로써 멀티스레드 환경에서도 한 번에 하나의 스레드만 카운터 값을 성공적으로 수정할 수 있게 됩니다.
다음은 위의 코드를 테스트하는 방법입니다:
public class TestMain {
public static void main(String[] args) throws InterruptedException {
CounterManager manager = new CounterManager();
Thread threadA = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
manager.increaseCounter();
}
});
Thread threadB = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
manager.increaseCounter();
}
});
threadA.start();
threadB.start();
threadA.join();
threadB.join();
System.out.println("Final Counter Value: " + manager.getCounterValue());
}
}
두 개의 스레드가 동시에 `increaseCounter()` 메소드를 호출하여 카운터를 증가시킵니다. 최종 출력값은 2000이 되어야 하며, CAS를 통해 멀티스레드 환경에서도 안전하게 카운터를 관리할 수 있음을 확인할 수 있습니다.