본문 바로가기
프로그래밍 언어

자바 동기화 | Synchronized(Monitor), Atomic Type

by 내기록 2022. 8. 14.
반응형

 

동기화란 공유 리소스에 대해 여러 쓰레드의 액세스를 제어하는 기능이다.

상호배제란 하나의 프로세스가 공유 자원을 사용할 때 다른 프로세스가 동일한 공유자원에 접근할 수 없도록 통제하는 것이다.

 

Java에서 제공하는 동기화 방법으로는  1) 개체에 Lock을 걸어 상호배제를 할 수 있는 Synchronized(Monitor)와
2) non-locking 방식인 Atomic Type이 있다.

 

 

1. Synchronized

Mutex

여러 쓰레드를 실행하는 환경에서 자원에 대한 접근을 통제하기 위한 동기화 기법이다.

 

- Boolean type의 Lock변수를 사용한다. 따라서 1개의 공유자원에 대한 접근을 제한한다.

- 공유 자원을 사용중인 쓰레드가 있을 때, 다른 쓰레드가 공유 자원에 접근한다면 Blocking 후 대기 큐로 보낸다.

- Lock을 건 쓰레드만 Lock을 해지할 수 있다.

 

Semaphore

멀티프로그래밍 환경에서 다수의 프로세스나 쓰레드가 n개의 공유 자원에 대한 접근을 제한하는 방법으로 사용되는 동기화 기법이다.

 

- 세마포어 변수를 통해 wait, signal을 관리한다. 세마포어 변수는 0 이상의 정수형 변수를 갖는다.

- n개의 공유자원에 대한 접근을 제한할 수 있으며 이를 계수 세마포어라고 한다.

- 접근 가능한 공유 자원의 수가 1개일 때는 이진 세마포어로 뮤텍스처럼 사용할 수 있다.

- 큐에 연결된 쓰레드를 깨우는 방식에 따라 강성 세마포어(큐에 연결된 쓰레드를 깨울 때 FIFO 정책 사용), 약성 세마포어(큐에 연결된 쓰레드를 깨울 때 순서를 특별히 명시하지 않음) 으로 구분된다.

- Lock을 걸지 않은 쓰레드도 Signal을 보내 Lock을 해제할 수 있다.

 

Monitor

JVM은 상호배제를 위해 Monitor를 사용한다. monitor는 임계 구역을 지켜내기 위한 상호 배제를 프로그램으로 구현한 것이다.

세마포어는 wait & signal 연산 순서를 바꿔서 실행하거나 둘 중 하나라도 생략하면 상호배제를 위반하는 상황이나 교착 상태가 발생한다.

wait&signal 연산이 프로그램 전체에 구성되어 있으면 세마포어의 영향이 미치는 곳이 어딘지 정확히 파악이 어렵기 때문에 세마포어를 사용하여 프로그램을 구현하는 것은 어렵다.

이러한 단점을 극복하기 위해 Monitor가 등장했다.

 

Java에서 제공하는 Monitor는 순차적으로 사용할 수 있는 공유 자원 혹은 공유 자원 그룹을 할당하는데 사용된다.

모니터는 이진 세마포어만 가능하다.

 

아래 이미지는 모니터를 통해 프로세스가 자원에 접근하는 방식이다.

https://tecoble.techcourse.co.kr/post/2021-10-23-java-synchronize/

공유 자원에 점유 중인 프로세스(스레드)는 Lock을 가지고 있다. 공유 자원을 점유 중인 쓰레드가 있는 상황에서 다른 쓰레드가 공유 자원에 접근하려고 하면 Monitor queue에서 진입을 wait 한다. Monitor는 세마포어처럼 signal 연산을 보내는 것이 아니라 조건 변수를 사용하여 특정 조건에 대해 대기 큐에 signal을 보내 작업을 시작시킨다.

 

Monitor의 구성

  • Thread단위로 Monitor lock을 획득하거나 반환한다 (acuire lock / release lock)
  • 동기화 코드를 수행할 때는 동기화 대상 인스턴스의 Monitor lock을 획득한 후 진입이 가능하며, 동기화 코드를 벗어날 때에는 Monitor lock을 반환한다.
  • 동기화 대상 인스턴스별로 결합된 Monitor가 존재한다.

 

Synchronized 키워드

자바 코드에서 동기화 영역은 synchronized 키워드로 구분된다.

 

자바의 모든 인스턴스는 Monitor를 가지고 있으며 (Object 내부)Monitor를 통해 Thread 동기화를 수행한다.

Synchronized 키워드가 붙은 메서드를 사용하려면 Lock을 가지고 있어야 한다.

 

synchronized를 사용하는 방법으로는 synchronized method와 synchronized block이 있다.

 

- Synchronized method

public synchronized void run(int value){
      this.count += value;
  }

- Synchronized block

synchronized (this) {
  ... 내용 ...
}

 

참고 : Java-혼동되는-synchronized-동기화-정리

 

 

wait, notify

Monitor에는 Condition Variable이 있는데 이를 통해 wait(), notify() 메서드가 구현되어 있다.

Lock을 가진 쓰레드가 다른 쓰레드에 Lock을 넘겨준 이후에 대기해야 한다면 wait()을 사용한다.

대기 중인 임의의 쓰레드를 깨우려면 notify()를 통해 깨울 수 있으며

대기 중인 모든 쓰레드를 깨우려면 notifyAll()을 통해 깨울 수 있다. 이 경우에는 하나의 쓰레드만 lock을 획득하고 나머지 쓰레드는 다시 대기 상태에 들어간다.

 

 

Synchronized의 문제점

synchronized는 blocking을 사용하여 멀티 스레드 환경에서 공유 객체를 동기화하는 키워드이다.

그러나 blocking을 사용하면 성능 이슈가 발생할 수 있다.

특정 스레드가 해당 블럭 전체에 lock을 걸면, 해당 lock에 접근하는 스레드들은 블로킹 상태에 들어가기 때문에 아무 작업도 하지 못한 채 자원을 낭비한다. 또한 blocking 상태의 스레드를 준비 혹은 실행 상태로 변경하기 위해 시스템의 자원을 사용해야 한다. 결국 이 문제는 성능 저하로 이어진다.

Atomic Type은 non-blocking으로 원자성을 보장하기 위한 방법이다.

 

 


2. Atomic Type

Synchronized와는 다르게 blocking이 아닌 non-blocking 방법을 사용하며 원자성을 보장한다.

Atomic Type은 단일 변수에 대해서 Atomic Operations을 지원한다.

원시 타입과 참조 타입 두 종류의 변수에 모두 적용이 가능하다. 사용 시 내부적으로 Compare-And-Swap(CAS) 알고리즘을 사용해 lock 없이 동기화 처리를 할 수 있다. 사용법은 변수를 선언할 때 Atomic 타입으로 선언 해주면 된다.

 

** 원시 타입(Primitive type) : int, long, char, doubl 등 실제 데이터 값을 저장하는 타입

** 참조 타입(Reference Type) : 원시 타입을 제외한 타입들 (문자열, 배열, 클래스, 인터페이스 등)

 

주요 Method

  • get() : 현재 값을 반환한다.
  • set(newValue) : newValue로 값을 업데이트한다.
  • getAndSet(newValue) : 원자적으로 값을 업데이트하고 원래의 값을 반환한다.
  • compareAndSet(expect, update) : 현재 값이 예상하는 값(=expect)과 동일하다면 값을 update 한 후 true를 반환한다.
    예상하는 값과 같지 않다면 update는 생략하고 false를 반환한다.
  • Number 타입의 경우 값의 연산을 할 수 있도록 addAndGet(delta), getAndAdd(delta), getAndDecrement(), getAndIncrement(), incrementAndGet() 등의 메서드를 추가로 제공한다.

 

class LockExample {
    private AtomicBoolean locked = new AtomicBoolean();

    public boolean tryLock() {
        if (!locked.get())  {
            // 작업 수행
        }

        return locked.compareAndSet(false, true);
    }
}

 

Compare-And-Swap(CAS)

https://lkhlkh23.tistory.com/131

Atomic은 CAS 알고리즘을 기반으로 동작한다.

CAS는 메모리 위치의 내용을 지정된 값과 비교하고 동일한 경우에만 해당 메모리 위치의 내용을 새로운 지정된 값으로 수정하는 것이다.

이 역할을 하는 메서드가 compareAndSet() 이다. 즉, synchronized 처럼 임계 영역으로 같은 시점에 두개 이상의 쓰레드가 접근하려고 할 때 쓰레드 자체를 block 시키는 메커니즘이 아니다.

 

현재 연산에서 기대하는 값과 메모리 상의 값이 일치하지 않는다면 '중간에 다른 쓰레드가 끼어든 것'으로 판단하여 write를 실패시키고 재시도를 하게된다. lock-free 방식으로 루프를 돌기 때문에 block<->unblock 상태 변경 처리보다 비용을 절감할 수 있다.

 

public class AtomicInteger extends Number implements java.io.Serializable {

        private static final Unsafe U = Unsafe.getUnsafe();
        private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value");
        private volatile int value;

        public final int incrementAndGet() {
                return U.getAndAddInt(this, VALUE, 1) + 1;
        }
}

public final class Unsafe {
        @HotSpotIntrinsicCandidate
        public final int getAndAddInt(Object o, long offset, int delta) {
                int v;
                do {
                      v = getIntVolatile(o, offset);
                } while (!weakCompareAndSetInt(o, offset, v, v + delta));
                        return v;
        }
}

위 코드에서 AtomicInteger를 보면 CAS 알고리즘의 로직이 구현되어 있다.

weakCompareAndSetInt() 메서드를 호출하여 메모리에 저장된 값과 현재 값이 동일하다면, 메모리에 변경한 값을 저장하고 true 값을 반환하여 while문을 빠져나온다.

 

! 여기서 value 변수에 volatile 이 붙어있는 것을 확인할 수 있다.

참고 : java-volatile

 

 

Atomic Type vs Synchronized 

Blocking된 Thread는 즉시 CPU를 다른 Thread에게 넘기는 반면 non-blocking된 Thread는 무한 루프를 돌면서 true를 반환 받을 때 까지 기다린다.

Synchronized 방식이 더 좋지 않을까 생각할 수 있지만 Thread 상태를 계속해서 바꿔주어야 하고(context switch) 그때마다 CPU 자원이 낭비되기 때문에 Atomic에 비해 성능이 느리다.

Atomic은 무한루프를 돌다가 true를 반환 받는 순간 thread의 상태를 변경하지 않고 바로 이어서 작업을 할 수 있다. 따라서 Synchronized 의 성능 이슈가 발생하지 않고 CAS 알고리즘을 통해 가시성과 원자성을 보장할 수 있다.

 

** 가시성 문제 : 멀티 쓰레드, 멀티 코어 환경에서 각 CPU는 메인 메모리의 값이 아닌 각 CPU의 캐시를 참조한다. 메인 메모리와 CPU 캐시 값이 다른 경우에 발생할 수 있는 문제를 가시성 문제라고 한다.

 

 

Conclusion

Thread-safe하지 않은 환경에서는 동기화를 사용해야 하는 경우가 있다.

싱글톤 패톤을 생각하면 제일 처음 싱글톤을 생성할 때 thread-safe 하지 않은데 이런 경우에 동기화가 필요하다.

Java에서는 synchronized와 Atomic type을 사용해서 동기화를 할 수 있다.

 

 

 

 

References

https://tecoble.techcourse.co.kr/post/2021-10-23-java-synchronize/

https://happy-coding-day.tistory.com/8

https://zion830.tistory.com/58

https://steady-coding.tistory.com/568

https://lkhlkh23.tistory.com/131

 

반응형

댓글