과정을 즐기자

Java의 synchronized, Lock Stripping과 Atomic Type 본문

Java

Java의 synchronized, Lock Stripping과 Atomic Type

320Hwany 2023. 10. 11. 11:25

자바는 멀티 쓰레드 기반의 동시성 프로그래밍을 지원합니다. 따라서 CPU 코어가 많아질 수록 이를 잘 활용할 수 있습니다.

하지만 멀티 쓰레드이기 때문에 반드시 동시성 문제가 따라옵니다.

이번 글에서는 Java에서 동시성 문제를 해결하기 위해 사용하는 synchronized, Lock Stripping, Atomic Type에 대해

알아보겠습니다.

synchronized

자바에서 synchronized 키워드는 메소드 또는 블록을 동기화하는데 사용합니다.

synchronized가 붙은 메소드나 블록은 멀티 쓰레드이더라도 동시에 하나의 쓰레드만 접근할 수 있도록 합니다.

public synchronized void myMethod() {
    // 하나의 쓰레드만 메소드 안에 있는 로직을 수행할 수 있다
}
public void myMethod() {
    synchronized(this) {
        // 하나의 쓰레드만 블록 안에 있는 로직을 수행할 수 있다
    }
}

여러 쓰레드가 동시에 같은 변수를 증가시키는 로직이 있다고 해보겠습니다.

아래와 같은 메소드는 100개의 쓰레드가 동시에 메소드를 실행하면 likes 변수가 100증가 한다고 보장할 수 없습니다.

public void plusOne() {
    this.likes += 1;
}

여기에 synchronized 키워드를 붙여주면 동시에 하나의 쓰레드만 접근할 수 있어 100개의 쓰레드가 동시에 메소드를 실행해도

likes 변수가 100증가 한다고 보장할 수 있습니다.

public synchronized void plusOneWithSynchronized() {
    this.likes += 1;
}

하지만 synchronized는 성능적으로 문제가 있습니다. 다른 쓰레드가 해당 메소드를 실행하고 있다면 다른 쓰레드들은

Blocking 된다는 것입니다. 그림으로 살펴보겠습니다.

Thread1, Thread2, Thread3가 모두 메소드를 실행하려고 할 때 3개의 쓰레드가 동시에 실행할 수 없습니다.

Thread1이 먼저 실행하고 있다고 하면 Thread2, Thread3는 실행을 위해 기다리는 Blocking 상태가 됩니다.

즉 CPU Core를 점유한 상태로 대기 중인 상태가 되는 것입니다. 이것이 바로 성능 저하로 이어지게 되는 것입니다.

 

참고로 이때 ConcurrentHashMap은 성능 저하를 줄이기 위해 synchronized를 메소드 전체가 아닌 블록 단위에 

적용하며 자료구조 전체에 Lock을 거는 방식이 아닌 기본적으로 16개의 노드로 나눠 특정 노드에만 Lock을 적용합니다.

즉, 자료 구조 관점에서 자료 구조 전체가 아닌 일부분에 락을 적용하는데 이를 Lock Stripping이라고 합니다.

Atomic Type

자바에서는 동시성 문제를 처리하기 위해 synchronized 키워드 말고 Atomic Type도 지원합니다.

Atomic Type은 CAS 알고리즘을 통해 Non-Blocking 하면서 가시성과 원자성을 보장해 동기화 문제를 해결합니다.

 

CAS 알고리즘은 다음 순서로 진행됩니다.

1. 인자로 기존 값과 변경할 값을 전달한다

2. 기존 값이 현재 메모리가 가지고 있는 값과 같다면 변경할 값을 반영하고 true를 반환한다

3. 기존 값과 현재 메모리가 가지고 있는 값이 다르다면 변경할 값을 반영하지 않고 false를 반환한다

 

이때 AtomicLong은 value가 volatile 키워드가 사용되어서 변수를 read, write 할 때 CPU 캐시가 아닌 메인 메모리에

직접 하기 때문에 메모리 가시성 문제를 해결하였습니다.

또한 CAS 알고리즘은 기존 값과 현재 메모리가 가지고 있는 값이 같을 때만 연산이 수행되어 원자성을 보장할 수 있습니다.

 

그림으로 살펴보겠습니다.

Thread1, Thread2, Thread3가 모두 메소드를 실행하려고 할 때 Thread1은 기존 값과 현재 메모리가 가지고 있는 값이

같아 변경할 값을 반영하고 true를 반환합니다. 하지만 Thread2, Thread 3는 기존 값과 현재 메모리가 가지고 있는 값이 달라

변경할 값을 반영하지 않고 false를 반환합니다.

synchronized와 다르게 Thread2, Thread3는 해당 값을 변경하기 위해 Thread가 Blocking 상태가 아닙니다.

Thread2, Thread3는 Non-Blocking 상태이며 다른 작업을 하다가 다시 해당 작업을 재시도를 할 수 있는 것입니다.

정리

지금까지 Java에서 동시성 문제를 해결하기 위해 사용하는 synchronized, Lock Stripping, Atomic Type에 대해

알아보았습니다.

 

synchronized는 해당 메소드나 블록을 동시에 하나의 쓰레드만 실행할 수 있도록 하여 동시성 문제를 해결했습니다.

Java에서 구현된 StringBuffer, HashTable, ConcurrentHashMap과 같은 클래스들은 메소드/블록 단위로

synchronized를 적용하여 멀티 쓰레드 환경에서도 동시성 문제가 발생하지 않도록 하였습니다.

Atomic type은 CAS 알고리즘을 통해 Non-Blocking 방식으로 가시성과 원자성을 보장해 동시성 문제를 해결하였습니다.

Java에서는 AtomicInteger, AtomicLong, AtomicBoolean, AtomicReference 등과 같은 Atomic 클래스가 있습니다.

 

ConcurrentHashMap은 synchronized 키워드를 블록 단위로 사용하며 기본적으로 노드를 16개로 나누며 

각 노드에 Lock을 걸고 이를 Lock Stripping이라고 했습니다.

또한 내부적으로도 AtomicType을 사용하여 동시성 문제를 해결하였습니다.

 

참고한 자료

 

[Java] atomic과 CAS 알고리즘

java-study에서 스터디를 진행하고 있습니다. synchronized의 문제점 synchronized는 blocking을사용하여 멀티 스레드 환경에서 공유 객체를 동기화하는 키워드이다. 그러나 blocking에는 여러 가지 단점이 존

steady-coding.tistory.com