지난 번에 설명했듯이 multi-thread 프로그래밍에서는 동기화가 중요합니다. 동일한 리소스에 대해 두 개 이상의 thread가 동시에 접근하여 기능을 수행하게 되면 오류가 발생할 수 있습니다. 여기서 오류란 thread 간의 동기화가 이루어지지 않아 코드 (내부적으로는 instruction) 수행의 결과가 우리가 예상했던 결과가 아닐 수 있다는 것입니다. (수행 오류에 대한 부분은 이전 포스트에서 설명하였습니다)  따라서 이러한 경우 thread가 리소스에 대해 순차적으로 접근할 수 있도록 동기화를 구현해야 합니다.

운영체제에서 동기화를 구현하는 방법은 세마포(Semaphore), 뮤텍스, 모니터 등 여러 방법들이 있지만, 여기서는 자바에서 제공되는 동기화 방법 위주로 설명하도록 하겠습니다.

일단 다음과 같은 예제를 살펴 보겠습니다.

다음 코드는 두 개의 thread – Park 과 ParkWife 가 Bank 라는 리소스를 공유하는 내용입니다.  

코드의 내용을 먼저 설명하면, Bank 에 10000원이 있었습니다. BankMain을 보면, 두 개의 thread Park과 ParkWife가 수행되고 있습니다. Park 이 하는 일은 3000원을 입금하는 일이고, ParkWife가 하는 일은 1000원을 출금을 합니다. 우리가 예상하는 money의 결과 값은 12000원입니다. 과연 실행의 결과가 그럴까요?

출력결과

결과를 보면 12000원이아닌 13000원 임을  알 수 있습니다. 왜 이런 결과가 나온 걸까요?

두 thread를 살펴보면 먼저 Park 이 실행되고, 0.2초 후에 ParkWife 가 실행됩니다. Park이 하는 일은 saveMoney() 메서드를 수행하죠. saveMoney() 메서드는 멤버 변수인 money 값을 가져와서 그 값을 더하여 반영하기 전에 3초간 쉽니다. 이때 0.2초 후에 시작한 ParkWife 에서 minusMoney() 메서드가 실행되는데 역시 멤버 변수인 money 값을 가져온 후 0.2초 쉬었다가 값을 출금하여 바로 반영합니다. 이때 money는 9000원이 되었습니다. 하지만 3초 뒤에 시작된 Park thread의 saveMoney() 메서드는 그 사이 변경된 money  값이 아닌 3초 전에 가져왔던 값(10000원)에 3000원을 더해주기 때문에 최종 money 변수 값은 13000원이 됩니다.

(물론 더하기 전에 멤버 변수값을 다시 가져오면 되지만, 이 예제는 다중 쓰레드 프로그램에서 발생할 수 있는 오류를 설명하기 위한 예제입니다.)

자 그럼 어떻게 처리해야 이러한 오류가 없을까요?

Park과 ParkWife 쓰레드는 동일한 리소스인 Bank에 접근하게 됩니다. 이때, Bank는 공유 리소스가 되는 것이고, 다중 쓰레드 프로그램에서는 공유 리소스에 접근할 때 순서대로 접근해야 합니다. 다시말해 Park의 saveMoney()가 수행되는 동안에는 다른 메서드는 Bank리소스에 접근 할 수 없어야 합니다. 

동기화를 구현하기 위해서 자바에서는 synchronized 메서드 방식과 synchronized 블록 두 가지 방식을 제공합니다. 

synchronized 메서드 방식은 공유 리소스에 접근하게되는 메서드에 synchronized 키워드를 사용하는 방법입니다. 위 예제에서는 saveMoney()메서드와 minusMoney()메서드에 사용하면 됩니다. 다음과 같이 코드를 수정해 보겠습니다.  

두 메서드에 synchronized 키워드를 사용하게 되면 하나의 메서드가 공유 리소스를 접근하는 동안 리소스에 lock이 걸리고 메서드의 수행이 끝날 때까지 다른 메서드는 해당 리소스에 접근할 수 없습니다. 결과 코드를 보면 saveMoney() 수행이후에 minusMoney()가 수행된 것을 알 수 있습니다

synchronized block 방식은 어떤 블럭을 {} 설정하고 그 블럭이 수행되는 동안 lock을 걸어야 하는 리소스를 명시하는 방식입니다.

각 메서드에 synchronized block을 적용해 보았습니다. 이때 공유 되는 리소스는 Bank 자신이므로 this를 써주면 됩니다. 

출력 결과를 보면 동기화가 잘 구현된 것을 알 수 있습니다.

출력 결과

만약 Bank 가 아닌 Thread 부분에서 synchronized block 을 구현한다면 공유 리소스는 무엇이 될까요? 메서드에 블럭을 빼고 thread 부분에 구현한 코드는 다음과 같습니다.

여기서는 공유 리소스가 this 가 아닌 BankMain.myBank 이므로 블럭에 this를 써주면 됩니다. 그럼 이 블럭{} 부분이 수행되는 동안 리소스에 lock이 걸리고 따라서 start minus 로그가 나중에 출력되는 것을 알 수 있습니다.

어떤 방식을 더 선호 하느냐는 경우에 따라 다르지만, 리소스에 lock  이 걸린다는 것은 그 만큼 수행에 지연이 발생하는 것이므로 메서드 전체보다는 블럭을 지정할 수 있다면 블럭에 설정하는 것이 더 효율적일 수 있습니다. 또한 Vector 나 HashTable의 경우 기본적으로 동기화를 제공하는 클래스인데 자바 소스 파일을 보면 모든 Vector와 HashTable의 모든 메서드에 synchronized 키워드가 써 있는것을 볼 수 있습니다. 이 경우는 단일 쓰레드 프로그램에서는 오버헤드가 발생되므로 ArrayList 나 HashMap을 사용하는 것을 권장 합니다.

여기까지 자바에서 기본적으로 제공되는 동기화 방법에 대해 알아보았습니다. 그럼 다음에는 여러 thread가 한정된 리소스 사용해야 할때 리소스가 없는 경우 기다리는 wait()과 리소스가 available한 경우 기다리는 thread를 깨우는 notify()/notifyAll() 메서드의 사용방법에 대해 알아보도록 하겠습니다.