일을 하면서 다음과 같은 상황이 있었다.
특정 카운트를 담기 위해 변수를 생성해두었고, 해당 변수를 이용하여 각 스레드에서 나온 각 카운트들을 더해주어야한다.
db쪽이 아닌 코드 내부에서 해결해야되는 문제이기때문에 방법을 찾다 Atomic을 이용하여 하드웨어 수준에서 스레드 안전성을 보장하는 방법을 알게되었다.
public static void main(String[] args) throws InterruptedException {
UnsafeCounter counter = new UnsafeCounter();
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = Thread.ofPlatform().start(() -> {
for (int j = 0; j < 10000; j++) {
counter.increment();
}
});
}
// 모든 스레드가 끝날 때까지 대기
for (Thread thread : threads) {
thread.join();
}
System.out.println("=== Unsafe Counter 결과 ===");
System.out.println("기대값: 100000");
System.out.println("실제값: " + counter.getCount());
}
위와 같이 각 스레드에서 외부 공유 변수를 이용하여 카운트를 증가시키는 코드가 존재한다.
해당 결과가 어떻게 나올까? 동일하게 100000?
기대값: 100000
실제값: 23703
실제로 값을 보게되면 기대값에 한참 못미치는 수준으로 떨어진다.
이유는 단순하다. 각 스레드가 외부 공유 변수에 동시에 접근하면서 동시성 문제가 발생하기 때문이다

각 스레드는 메모리에서 공유 변수의 값을 READ -> +1 -> WRITE 하는 순으로 값을 추가하게된다
여기서 다른 스레드가 거의 동시에 해당 변수에 같이 접근을 하게된다면, 스레드 1이 WRITE 하기전에 READ를 하기때문에 결과적으로는 공유 변수에는 1이 저장되게된다.
Atomic은 하드웨어 수준에서의 원자적 연산을 활용하여 공유 변수에서 스레드 안전성을 보장한다고 클로드씨가 말한다.
CAS(Compare-And-Swap) 알고리즘을 이용하여 동작이 되는데 간단하게 설명하면 아래와 같다.
사실 원자적 수행을 그렇게 깊게 알 필요는 없는거같고, 이제 사용해보자
public static void main(String[] args) throws InterruptedException {
AtomicInteger counter = new AtomicInteger();
Thread[] threads = new Thread[10];
long start = System.nanoTime();
for (int i = 0; i < 10; i++) {
threads[i] = Thread.ofPlatform().start(() -> {
for (int j = 0; j < 10000; j++) {
counter.incrementAndGet();
}
});
}
// 모든 스레드가 끝날 때까지 대기
for (Thread thread : threads) {
thread.join();
}
System.out.println("=== Unsafe Counter 결과 ===");
System.out.println("기대값: 100000");
System.out.println("실제값: " + counter.get());
}
아까와 코드는 동일하지만 AtomicInteger로 객체가 바뀌었다.
결과를 살펴보면
기대값: 100000
실제값: 100000
완벽하게 일치한다.
이와같이 스레드에서 외부 공유변수 값을 사용할때는 Atomic으로 원자성으로 값을 보장하자
단순하게 단일 변수에서 연산을 할때는 Atomic이 유리하지만, 여러 변수에서 값이 연산될경우 Atomic으로 하게되면 두 변수를 따로따로 계속 업데이트 하기때문에 대기되는 경우가 많아질 수 있어서 응답 속도가 느릴 수 있다.
이때문에 변수가 여러개일경우 synchronized를 사용하는것이 더 안전한 경우도 있다