본문 바로가기

프로그래밍(TA, AA)/JVM 언어

[병렬프로그래밍] 멀티 스레드와 동기화

멀티 스레드와 동기화


스레드를 사용하게되면 필연적으로 만나게되는 문제가 바로 동기화(Synchronization)입니다. 아래는 여러개의 스레드가 실행되는 과정을 도식화한 그림입니다. 여러개의 스레드들이 있다면 이 스레드들은 서로 번갈아가면서 실행이 될 것입니다. CPU가 하나이기때문에 OS의 스케줄링 정책에 따라 쓰레드를 조금씩 실행시키는 방식이 됩니다. 어떤 스레드가 실행될지는 OS 스케줄러만 알고 있습니다.



- 애플리케이션에서 하나 이상의 스레드 사용시 새로운 스레드를 한개라도 생성해서 운영한다면 멀티스레드 프로그램이 됩니다.

- 스레드는 별도로 실행되는 하나의 실행이므로 동시에 여러 스레드가 실행되면 스레드들이 공유하는 자원에 대해 동기화가 발생할 수 있습니다.


만약 스레드들이 서로 침범하는 영역없이 동작한다면 동기화 문제를 고려하지 않아도 됩니다. 프로세스들 간에 대해서는 이미 OS가 별도의 프로그램으로 동작하도록 알아서 처리를 해주고 있습니다. 프로세스는 자기만에 address space 범위를 갖고있고, 다른 프로세스들이 차지하는 메모리 스페이스를 침범할 수가 없습니다. 한 프로세스 안에서 실행되는 여러개의 스레드들은 서로 공유할 수 있는 메모리 부분이 있습니다. 공유할 수 있는 부분이 있으므로, 그때 동기화문제가 발생할 수 있습니다. 



자바의 런타임 데이터 영역들과 공유 데이터

동기화 문제는 스레드들간의 공유자원에 접근하는 것은 한번에 한 스레드만 가능하게 함으로써 해결할 수 있습니다. 한번에 한 스레드만 공유자원에 액세스하도록 만들어주면 동기화문제 해결이 됩니다. 자바런타임시에는 쓰레드들간 아래의 영역이 공유됩니다. 이부분에 대한 제어가 필요합니다.


아래 항목들은 스레드들간 공유되는 영역도 있고, 독립적인 영역도 있습니다. 개발자가 확인해야할 부분은 공유되는 영역 부분입니다. 모든 스레드들은 각각의 Program Counter(일종의 지시자, 다음 수행되야할 명령어강 무엇인지를 가르키고 있음.)를 가지고 있습니다. 


- PC(Program Counter) 레지스터 영역: 스레드 각각이 갖고 있으므로 스레드간 독립적인 영역

- JVM 스택 영역: 지역변수, 파라미터, 리턴값, 객체 레퍼런스들이 저장되어 있는 공간. 스레드 각각이 갖고있는 따로 관리되는 독립적인 영역

- 힙 영역: 생성된 자바의 객체들이 저장되는 부분. 모든 스레드들이 접근 가능합니다. (공유되는 영역)

- 메소드 영역: 함수영역으로 각 클래스 또는 인터페이스에 런타임 컨텍스트 풀영역, 메소드 생성자를 저장하는 영역으로 모든 스레드에 의해서 공유되는 영역 입니다.

- 런타임 컨텍스트 풀 영역: 클래스 또는 인터페이스의 클래스변수, 스태틱변수, 클래스 객체의 레퍼런스를 저장하는 영역으로 스레드들간 공유되는 영역입니다.

- 데이터와 메소드 스택 영역: 마찬가지로 스레드들간 공유되는 영역입니다.


공유되는 데이터 영역의 포함 관계는 아래와 같습니다.

객체(힙 영역) > 메소드(메소드 영역) > 변수(런타임 컨텍스트 풀 영역)


동기화를 하기위해서는 이 공유되는 영역을 보호해줄 수 있는 장치가 필요합니다. 개발자들이 짠 코드 중에 일부 파트를 여러 스레드들이 실행가능한 경우에 이 특정부분을 한번에 한 스레드만 실행되도록 만들어주려면 특정영역을 보호해주는 장치가 필요합니다.


- 임계영역이란 멀티 스레드에 의해 공유자원이 참조할 수 있는 코드의 범위를 말합니다.

  즉, 한번에 한 쓰레드만 접근이 가능한 영역을 임계영역이라 합니다.

- 멀티 스레드 프로그램에서 임계영역을 처리하는 경우 심각한 문제가 발생할 수 있습니다.

- 이러한 상황을 해결할 수 있는 방법이 동기화를 이용하는 것입니다.

- 동기화를 처리하기 위해 모든 객체에 락(lock)을 포함 시킵니다.

- 이란 공유 객체에 여러 스레드가 동시에 접근하지 못하도록 하기 위한 것으로 모든 객체가 힙 영역에 생성될때 자동으로 만들어집니다.


자바에서는 임계영역을 보호할 수 있는 메커니즘만 제공해줄 뿐, 실제 쓰레드들이 잘 동기화되도록하는 것은 개발자가 책임져야할 부분입니다.



lock, monitor, synchronized

- 동기화 문제가 발생하는 최소 단위는 객체이며, 문제 발생 시점은 지점은 객체가 소유한 내부 변수가 됩니다.

- 자바는 동기화 문제를 해결하기 위해 모든 객체에 락(lock or 세마포어)를 포함시킵니다.

- 락은 모든 객체가 인스턴스화 될때 힙 영역에 객체가 저장될때 자동으로 생성됩니다.

- 락은 보통의 경우에는 사용되지 않으며 동기화가 필요한 부분에서 synchronized 키워드를 사용합니다. (2가지 방법 : 메소드 선언 or 블럭 선언)

   synchronized는 자바 키워드로 synchronized로 묶인 메소드를 보호 구역이라고 말할 수 있습니다.

- 모니터(monitor)

    synchronized 키워드를 사용하면 해당 객체의 락을 검사합니다.

    락의 현재 사용 여부를 검사함으로써 각 객체를 보호합니다.



1) 스레드가 synchronized 키워드를 사용한 메소드나 블록에 접근할시 연관된 모니터는 해당 객체의 레퍼런스 검사(스레드가 보호구역에 들어가도되는지 안되는지)하게 됩니다.

2) lock이 다른 어떤 스레드에게 사용하지 않고 있다면 JVM에게 알려줍니다.

3) JVM은 'monitorenter'라는 JVM 내부명령으로 해당 객체의 락을 요청한 스레드에게 줍니다.

4) 반대로 lock이 반환될때까지 진행되지 않고 스레드는 대기합니다.

5) 스레드가 lock을 얻은 후에 sychronized 메소드나 블록을 다 마치고 나면 'monitorexit'이라는 내부 명령으로 실행해서 해당 스레드가 얻은 객체의 lock을 즉시 반환한다.




결론적으로, 보호구역에서는 한번에 한스레드만 접근 가능하게 됩니다.


synchronized 동기화 방법입니다.

// 1) 메소드의 동기화 방법
public synchronized void Method() {
// 임계영역 코딩
}
// 2) 특정 블록의 동기화 방법
public void normalMethod() {
synchronzied(/* 동기화할 객체 또는 클래스명 */) {
// 임계영역 코딩
}

}

메소드의 일부분만 동기화하도록 하고싶을경우 특정 블록 동기화 방법을 이용하면 됩니다.



wait(), notify(), notifyAll()

- 동기화된 스레드(보호구역에서 실행하고있는 스레드) 혼자서는 동기화 블록(보호구역 안)에서 다른 스레드에게 제어권을 넘기지 못합니다.

- 동기화된 블록에서 스레드간의 통신(제어권을 넘김)하기 위해서는 wait(), notify(), notifyAll() 메소드를 사용해야 합니다.

  (여기서의 통신은 동기화에 대한 통신을 말합니다.)

- 메소드를 사용할 때 주의해야할 점은 synchronized 블록에서만 의미가 있습니다.

- Synchronized 블록이 아닌 경우에 사용할 경우 java.lang.illegalMonitorStateException이 발생합니다.

- wait() 메소드는 어떤 객체에 대해 스레드를 대기하게 만듭니다.

- notify() 메소드는 객체에 대해 대기중인 스레드가 있을 경우 우선순위가 높은 스레드 하나만을 깨웁니다.

- notifyAll() 메소드는 대기중인 스레드 전부를 깨웁니다.