Java
에서 애플리케이션 개발을 진행하다 보면 Multi Thread
를 활용한 프로그래밍을 경험하게 된다.
이로인해 Single Thread
로 개발을 진행할 경우에는 발생하지 않았던 몇몇 문제점들이 발생하는데 그 중 하나가 RaceCondtiion
이다.
RaceCondition
이란 공유 자원에 대해 여러개의 Thread
또는 Process
가 동시에 접근하기 위해 경쟁
하는 상태를 의미한다.
Multi Thread
는 말 그대로 하나의 Process
에서 Thread
를 여러개 사용하는 것을 말한다.
이 때, Process
내에 존재하는 Thread
는 메모리의 Heap
영역과 Data
영역을 공유하게 된다.
이 메모리를 공유하는 것은 적절하게 사용한다면 Context Switching
에 사용되는 비용을 최소화해 애플리케이션의 성능을 향상시킬 수 있다.
하지만, 부적절하게 사용한다면 여러 Thread
들이 공유된 특정 자원에 동시에 접근할 경우 문제가 발생할 수 있는데, 이 때 발생할 수 있는 문제점 중 하나가 Race Condition
이다.
예를 들어 아래와 같은 코드가 있다고 해보자.
int a = 1;
a = a + 1;
이 때, 프로그램이 동작하는 순서는 다음과 같다.
a
를 메모리에 올린다.a
에 1을 더한다.만약, 이 프로그램이 Single Thread
에서 동작한다면 이 프로그램에 큰 문제는 없다.
그런데, 만약 이 프로그램이 Multi Thread
에서 동작하고 있다면 문제가 발생할 수 있다.
만약 2개의 Thread
가 존재하고 2개의 Thread
모두 a
에 1을 더하는 동작을 수행한다고 생각해보자.
위의 표와 같이 동작한다면 우리는 a
에 1을 더하는 작업을 두번 하는 코드를 통해 기대하고 있던 3이라는 결과값을 얻을 수 있을것이다. 하지만, Java
에서 여러개의 Thread
가 동시에 실행될때는 어떤 Thread
가 먼저 실행될지 알 수 없다.
그렇기 때문에 위와 같은 결과가 나올 수 있다.
Thread1
에서 a
를 메모리에 올리고 1을 더하는 작업이 수행 되었지만, Context Switching
이 발생하여 Thread2
가 동작하면서 Thread1
이 a
에 값을 저장하지 않았으므로 다시 1을 가져와서 계산을 한 후 2라는 결과값을 저장한다.
그 다음 다시 Context Switching
이 발생하면 Thread1
은 기존에 a
에 1을 저장하는 동작을 수행했으니 다음 동작인 결과를 저장하는 동작을 수행한다.
이 때, Thread1
이 가지고 있던 값은 기존 a
에 1을 더한 값인 2이므로 다시 2가 저장된다.
우리는 3이라는 결과값을 기대했지만, Context Switching
이 발생하면서 2라는 우리가 기대하지 않은 값을 얻게되었으며, 이와 같은 문제를 Race Condition
이라고 한다.
Race Condition
을 해결하기 위해서는 임계 구역을 가진 Thread
들이 서로 겹치지 않고 단독으로 실행 즉 상호 배제
(Mutual Exclution
)되는 것이 보장되어야 한다.
그렇다면 이 Race Condition
을 해결하기 위한 방법은 어떤것이 있을까?
첫 번째 방법은 Mutex
이다.
Mutex
를 기반으로 한 상호 배제에서는 Key
를 통해 리소스로의 접근을 관리한다.
Thread
가 공유된 리소스에 접근하기 위해서는 Key
를 획득해야 하며, Key
를 가지고 있지 않은 Thread
는 다른 Thread
가 리소스를 사용한 뒤 Key
를 반납하기를 기다려야 한다.
이 때, 이 Key
는 동기화(Synchronization
)또는 락(Lock
)을 통해 구현한다.
두 번째 방법은 Semaphore
이다.
Semaphore
에서는 Key
는 존재하지 않지만, 특정 리소스에 대해 접근할 수 있는 Thread
의 최대 수를 관리하는 값이 존재한다.
각 Thread
들은 Share Resource
영역에 있는 값을 사용하기 전에 먼저 Semaphore
의 값을 확인한다. 이 때, Semaphore
의 값이 0이면 리소스를 사용할 수 없으며 다른 Thread
가 리소스를 사용 완료한 뒤 Semaphore
의 값이 증가하여 0이 아니게 되면 리소스를 사용할 수 있다.
그렇다면 두 방식의 공통점과 차이점은 무엇일까?
먼저 공통점은 Thread
의 독립적 실행 즉 상호 배제
를 달성하기 위한 방법이라는 것이다.
차이점은 아래와 같다.