synchronized

appti·2024년 2월 27일
0

분석

목록 보기
2/25

서론

synchronized는 자바에서 동시성 문제를 해결하기 위해 사용할 수 있는 간단한 동기화 기법입니다.

단순히 키워드를 추가하거나 블록을 만들면 사용할 수 있기 때문입니다.

다만, 사용법과 달리 synchronized에 적용된 이론이나 동작 과정은 생각보다 복잡합니다.
이를 제대로 이해하기 위해 synchronized와 관련된 내용을 분석하고 정리했습니다.

사전 지식

뮤텍스

Mutex(Mutual Exclusion)으로, 상호 배제라는 의미를 가지고 있습니다.

여러 스레드가 동시에 임계 영역에서 공유되는 리소스에 접근해 동시성 문제가 발생하는 것을 방지하는 동기화 기법 중에 하나입니다.

상호 배제라는 의미처럼, 하나의 스레드가 플래그(= 락)을 획득해 임계 영역에서 작업을 수행하고 있으면 다른 스레드는 접근하지 못합니다.

모니터

모니터(Monitor) 또한 동기화 기법 중 하나로, 뮤텍스나 세마포어보다 더 고수준(= 더욱 추상화 되어 있음)의 동기화 기법입니다.

다음과 같은 방식으로 동시성 문제를 해결합니다.

  • 상호 배제
    • 뮤텍스와 동일합니다.
    • 모니터에서 사용하는 상호 배제 방식은 뮤텍스를 의미합니다.
  • 상호 협력
    • 모니터의 조건 변수(Condition Variable)을 통해 여러 스레드가 수행해야 하는 작업을 위해 협력하면서 동기화 기법을 적용하는 것을 의미합니다.
    • A 스레드가 특정 조건을 만족하지 못해 대기하게 되면, B 스레드가 특정 조건을 만족시킨 뒤 A 스레드를 깨우는 방식입니다.

모니터 조건 변수 종류

모니터에서 스레드의 역할은 대기 중인 스레드(wait)와 락을 가진 상태로 대기 중인 스레드를 깨우는 스레드(signal)로 구분할 수 있습니다.
(앞으로 각각 wait 스레드, signal 스레드라고 명칭하겠습니다.)

이 때, 어떤 종류의 스레드가 먼저 모니터를 가질지에 따라 조건 변수의 종류를 구분할 수 있습니다.

모니터 조건 변수 종류와 동작 방식은 다음과 같습니다.

  • Signal and Wait
    • signal 스레드가 wait 스레드를 깨웁니다.
      • 이 때, signal 스레드는 락을 반납하고 대기 상태에 빠집니다.
    • 깨어난 wait 스레드는 락을 획득하고 임계 영역에서 작업을 모두 수행합니다.
    • 깨어난 wait 스레드가 작업을 모두 마치고 락을 반납하면, signal 스레드가 락을 획득하고 임계 영역에서 작업을 모두 실행합니다.
      • wait 스레드를 깨운 signal 스레드가 락을 획득해야 하기 때문에, 깨어난 wait 스레드가 반납한 락을 반드시 signal 스레드가 획득할 수 있도록 원자적 실행이 보장되어야 합니다.
  • Signal and Continue
    • signal 스레드가 wait 스레드를 깨웁니다.
      • 이 때, 깨어난 wait 스레드는 상호 배제와 동일하게 락을 획득하기 위해 경쟁합니다.
    • signal 스레드가 임계 영역에서 작업을 모두 실행하고 락을 반납합니다.
    • 깨어난 wait 스레드는 락을 가지기 위해 경쟁합니다.

자바에서의 모니터

자바에서는 모든 객체가 모니터를 가지게 됩니다.(Intrinsic Locks 혹은 Monitor Locks, Monitor라고 표현합니다.)
모든 객체는 Object 객체를 상속하므로, 모니터는 Object에 의해 관리됩니다.

이는 Object 클래스 문서에서 확인할 수 있습니다.
모니터와 관련된 notify(), notifyAll(), wait() 메서드를 확인할 수 있습니다.

메서드의 설명에 더 명확하게 확인할 수 있습니다.

자바에서의 모니터 구성

자바에서의 모니터는 다음과 같이 구성됩니다.

  • Entry Set
    • 상호 배제(뮤텍스)에 의해 대기하는 스레드가 관리되는 곳입니다.
    • 락을 획득할 때 까지 스레드가 대기합니다.
  • Wait Set
    • 상호 협력에 의해 대기하는 스레드가 관리되는 곳입니다.
    • signal 스레드가 깨우기 전 까지 대기합니다.

자바에서의 모니터 조건 변수

자바에서 모니터 조건 변수로 Signal and Wait, Signal and Continue 중 Signal and Continue를 사용합니다.

또한, OS 레벨의 모니터 조건 변수는 여러 개를 가질 수 있지만 자바에서의 모니터는 단 하나의 조건 변수를 가지고 있습니다.

이로 인해 OS 레벨의 모니터보다는 유연하지 못하지만, Object.notifyAll()로 Entry Set에서 대기하고 있는 모든 스레드를 깨울 수 있습니다.

대신 유연하게 스레드를 관리하지 못한다는 단점이 있습니다.
유연하게 스레드를 관리해야 할 경우, synchronized 대신 Lock을 활용해 처리할 수 있습니다.

synchronized 정의

자바의 synchronized는 가장 간단하게 동기화 기법을 적용할 수 있는 키워드입니다.
암시적으로 모니터를 통해 관리하게 됩니다.

synchronized 종류

synchronized 종류는 다음과 같이 크게 두 가지로 나뉩니다.

  • synchronized method
    • instance
    • static
  • synchronized block
    • instance
    • class

synchronized method

synchronized method는 메서드 시그니처에 synchronized 키워드를 추가하는 방식입니다.

instance

synchronized instance method는 일반 메서드에 synchronized 키워드를 추가하는 방식입니다.

public synchronized void method() {
    // 임계 영역
}

이 때 사용되는 락은 해당 메서드가 실행되는 인스턴스 자기 자신을 의미합니다.
즉, this에서 락을 가져옵니다.

static

synchronized static method는 static 메서드 시그니처에 synchronized 키워드를 추가하는 방식입니다.

public static synchronized void method() {
    // 임계 영역
}

이 때 사용되는 락은 해당 메서드가 위치한 클래스를 의미합니다.
즉 클래스에서 락을 가져옵니다.

락 구분

public synchronized void method1() {
    // 임계 영역
}

public static synchronized void method2() {
    // 임계 영역
}

이 경우 method1과 method2는 서로 다른 모니터를 가지게 됩니다.

동작 방식

synchronized method는 bytecodeInterpreter에서 메서드를 분석하고 실행할 때, synchronized 키워드 여부를 확인하고 락을 획득합니다.

이 과정에서 biased lock(= 원자적 연산을 수행하지 않는 락)을 사용할 수 있는지 여부를 판단하고 사용할 수 있다면 biased lock을 사용하고, 아닐 경우 synchronized block과 동일한 방식으로 락을 사용합니다.

synchronized block

synchronized block은 코드 블록에 synchronizd 키워드를 통해 임계 영역을 설정하는 방법입니다.

instance

synchronized instance block은 코드 블럭에 인스턴스를 지정하는 방식입니다.

Object lock = new Object();

public void method() {
    synchronized (lock) {
        // 임계 영역
    }
}

이 때 사용되는 락은 코드 블록에 지정한 인스턴스를 의미합니다.

static

synchronized static block은 코드 블록을 통해 클래스를 지정하는 방식입니다.

public void method() {
    synchronized (Class.class) {
        // 임계 영역
    }
}

이 때 사용되는 락은 코드 블록에 지정한 위치한 클래스를 의미합니다.

락 구분

Object lock = new Object();

public void method1() {
    synchronized (lock) {
        // 임계 영역
    }
}

public void method2() {
    synchronized (Class.class) {
        // 임계 영역
    }
}

이 경우 method1과 method2는 서로 다른 모니터를 가지게 됩니다.

동작 방식

synchronized block의 경우 모니터에서 락을 획득하고 해제하는 과정이 추가됩니다.

public class Main {
    public void method1() {
        synchronized (this) {
            // 임계 영역
        }
    }
}

위와 같은 예제 코드를 컴파일한 뒤 바이트 코드를 출력하면 다음과 같은 내용을 확인할 수 있습니다.

monitorenter의 경우 모니터에 진입해 락을 획득하며, monitorexit의 경우 락을 해제하고 모니터에서 빠져나옵니다.

컴파일을 했을 때 추가되는 만큼 자바 컴파일러가 이를 추가하며, 이와 관련된 내용은 interpreterRuntime.cpp에서 확인할 수 있습니다.

method + block

각각 별도의 모니터를 가지고 있으므로, 다음과 같이 총 4개의 락을 가질 수 있습니다.

  • synchronized instance method
  • synchronized static method
  • synchronized instance block
  • synchronized class block

synchronized 특징

재진입성

이미 모니터 영역에 들어가 락을 획득했다면, 이후 동일한 모니터 영역에 들어갈 때 락 획득 절차를 생략하고 바로 진입이 가능합니다.
이를 모니터 재진입이라고도 표현합니다.

class Super {
    public synchronized void method1() {
        // 임계 영역
    }
}

class Sub extends Super {
    public synchronized void method2() {
        super.method1();
    }
}

Sub sub = new Sub();
sub.method2();

자바는 모니터를 Object를 통해 관리합니다.

그러므로 Object -> Super -> Sub의 계층 구조를 가지고 있는 경우, 결과적으로는 모두 동일한 모니터를 갖게 됩니다.

하지만 코드 상으로는 두 개의 synchronized가 분리되었으므로, 모니터 진입 시점은 Sub 한 번, Super 한 번이 됩니다.

이 경우 Sub에서 이미 모니터에 진입해 락을 획득했으므로, Super에서 synchronized 영역에 진입할 경우 락 획득 시도를 생략하고 즉시 접근이 가능합니다.

그 외의 특징

  • 가시성
    • synchronized는 가시성을 지원하므로 멀티 스레드 환경에서 작업 시 CPU가 참조하고 있는 메모리가 달라 데이터 불일치가 발생하는 일을 방지할 수 있습니다.
  • 비공정성
    • synchronized 영역에 진입하지 못한 다른 스레드가 다시 경쟁해서 모니터를 획득하는 것은 순서가 정해져 있지 않습니다.
    • 이로 인해 기아 상태에 빠진 스레드가 나올 수 있지만, 이는 OS 레벨에서 적절하게 처리하게 됩니다.
  • sleep()
    • synchronized 영역에서 sleep()을 호출하더라도 락을 해제하지 않습니다.

synchronized 동작 방식

상호 배제

  1. 스레드 T1이 synchronized 영역에 접근해 Entry Set에서 대기
  2. 뮤텍스 락 획득 시도 -> 최초 실행이므로 바로 락 획득
  3. 락을 획득했기 때문에 임계 영역에 접근

  1. 스레드 T2가 synchronized 영역에 접근해 Entry Set에서 대기
  2. 뮤텍스 락 획득 시도
  3. 이미 락을 가진 T1이 임계 영역에서 작업 중이므로 BLOCK

  1. T1이 임계 영역에서 작업을 모두 마치고 모니터 해제
    7-1. 모니터를 해제할 수 있는 방법은 작업을 완료하거나 wait()를 실행하는 것 뿐입니다.
  2. Entry Set에서 대기하던 T2가 뮤텍스 락 획득 시도
  3. 락을 획득한 스레드가 없으므로 T2가 임계 영역에 도달해 작업 수행

결과적으로는 T1은 작업을 모두 마쳤고, T2는 새롭게 임계 영역에 도달했습니다.
이와 같이 모니터는 기본적으로 상호 배제 기반으로 동작합니다.

상호 협력

  1. T2가 특정 조건(= Condition Variable)에 만족하지 못해 wait() 호출
  2. wait()를 호출한 T2가 Wait Set에서 대기

이렇게 wait()를 호출해 Wait Set에 대기하는 스레드는 이후 다른 스레드가 notify() 혹은 notifyAll()을 호출하기 전 까지는 깨어나지 않습니다.

그러므로 지금 과정까지는 상호 협력과 상호 배제는 별도로 동작이 가능합니다.
시간이 경과해 위 그림과 같은 상황이라고 가정합니다.

  • T2는 그대로 Wait Set에 대기 중
  • T3는 뮤텍스 락을 획득하지 못해 Entry Set에 대기 중
  • T4는 뮤텍스 락을 획득해 임계 영역에서 작업 수행 중

  1. T4가 특정 조건을 만족시키고(= Condition Variable) Wait Set에 대기하고 있던 스레드를 깨우기 위해 notify()/notifyAll() 호출

  1. 특정 조건을 만족시키게 된, Wait Set에 대기하고 있던 T2가 깨어나 Entry Set에 추가

  1. 임계 영역에서 모든 작업을 수행한 T4가 모니터 해제
    5-1. 자바 모니터는 Signal and Continue이므로 notify()/notifyAll()을 호출하더라도 모니터를 해제하지 않고 계속 작업을 수행합니다.
  2. Entry Set에서 대기하고 있던 T2, T3이 뮤텍스 락을 획득하기 위해서 경쟁
  3. 락을 획득한 스레드가 없으므로 T2, T3 중 하나의 스레드가 락을 획득해 임계 영역에 도달

synchronized의 특성 중 하나는 비공정성이기 때문에 T2가 T3보다 오래 기다렸지만 T3이 먼저 실행될 수 있으므로, T3이 임계 영역에 도달했다고 처리했습니다.
(Entry Set과 Wait Set에서 다음 스레드를 선택하는 기준은 OS 스케줄러에 의해 결정됩니다.)

이러한 과정을 통해 synchronized가 동기화 기법을 적용해 스레드를 관리합니다.

결론

  • synchronized 키워드는 암시적으로 모니터를 통해 동기화 기법을 적용합니다.
  • 자바의 모니터는 상호 배제, 상호 협력을 통해 동기화 기법을 적용합니다.

참고

Object docs (java 8)
Intrinsic Locks and Synchronization
bytecodeInterpreter.cpp (openjdk)
interpreterRuntime.cpp (openjdk)
monitorenter, monitorexit (java se 6)

profile
안녕하세요

0개의 댓글