JAVA

[JAVA] 자바 쓰레드 동기화(1) - synchronized, wait()/notify()

jhkimmm 2022. 7. 5. 00:40

멀티 쓰레드 환경에서 Critical section 문제를 해결하기 위해서는 Critical section에 대해 쓰레드를 동기화 해야합니다.

자바에서는 아래 두 가지 방법으로 쓰레드를 동기화 할 수 있습니다.

1) synchronized 키워드 이용한 암묵적인 동기화

2) java.util.concurrent.locks 패키지의 lock 클래스들을 이용한 명시적 동기화

 

이번 포스팅에서는 synchronized 블럭을 이용한 암묵적인 동기화에 대해 정리하겠습니다.

기본적인 사용법

synchronized는 키워드는 Critical section을 설정하는데 사용되며 두가지 방식이 있습니다.

1. 메서드 전체를 Critical section으로 지정

public synchronized void exampleMethod(){
	//...
}

메서드 전체가 Critical section으로 지정되며, 쓰레드는 해당 메서드가 호출된 시점부터 해당 메서드가 포함된 객체의 lock을 얻어 작업을 수행하고 작업이 완료되면 lock을 반환합니다.

 

2. 특정 영역을 Critical section으로 지정 

synchronized(객체의 참조변수){
	//...
}
 

메서드 내의 코드 일부를 블럭으로 감싸고 Critical section으로 지정하는 방식입니다.

이때 참조변수는 락을 걸고자 하는 객체를 참조하는 것 이어야하며, 이 블럭에 진입할때 lock을 얻고 벗어나면 lock을 반환합니다.

 

모든 객체는 lcok을 하나씩 가지고 있으며, 해당 객체의 lock을 가지고 있는 쓰레드만 Critical section의 코드를 수행할 수 있습니다.

 

가능하면 메서드 전체에 lock을 거는 것 보다 synchronized 블럭을 활용하여 임계 영역을 최소화 하는 것이 좀 더 효율적입니다.

wait() & notify()

- 개요

wait()와 notify()는 Object에 정의되어 있으며, synchronized 블럭 내부에서만 사용할 수 있습니다.

 

동기화된 Critical section의 코드를 수행하다가 작업을 더 이상 수행할 수 없다면, 일단 wait()를 호출해 쓰레드가 lock을 반납하고 기다리게 합니다. 그러면 다른 쓰레드가 lock을 얻어서 해당 객체에 대한 작업을 수행할 수 있게 됩니다.

나중에 작업을 진행할 수 있는 상황이 되면 notify()를 호출해서 작업을 중단했던 쓰레드가 다시 lock을 얻어 작업을 진행할 수 있습니다.

 

이러한 방식을 통해 wait()와 notify()는 보다 효율적인 동기화를 가능하게 합니다.

 

- 작동원리

wait()가 호출되면 실행 중이던 쓰레드는 해당 객체의 waiting pool에서 notify()를 기다리고, notify()가 호출되면 해당 객체의 waiting pool에 있던 쓰레드 중에서 임의의 쓰레드 하나만 통지를 받습니다.

 

wait()에 매개변수로 대기시간을 설정하면, 지정된 시간동안만 기다립니다. 즉, 지정된 시간이 지난 후에 자동적으로 notify()가 호출되는 것과 같습니다.

 

- 문제점

1) Race Condition(경쟁 상태)

notify()는 그저 waiting pool에서 대기중인 쓰레드 중에 하나를 임의로 선택해서 통지할 뿐이고, 여러 쓰레드가 lock를 얻기 위해 경쟁하게 됩니다.

이를 위해서는 쓰레드를 구별해서 통지하는 등 동기화를 세세하게 컨트롤 해야하며, 이럴 경우에는 java.util.concurrent.locks 패키지의 lock 클래스들을 이용해야합니다.

 

2) Starvation(기아 현상)

운이 나쁘면 특정 쓰레드는 계속 notify를 받지 못하고 오래 기다리는 기아 현상이 발생할 수 있습니다.

이를 막으려면 notifyAll()을 사용해서 모든 쓰레드에게 통지를 해야합니다. 단, 모든 쓰레드에게 통지를 해도 결국 lock을 얻을 수 있는 쓰레드는 하나뿐임을 주의해야합니다.