일상 생활에서 쉽게 동기화라는 말을 자주 접할 수 있다. 구글이나 아이클라우드 등을 사용하면 자주 들을 수 있으며, 이로 인해 흔히 공유 자원을 일치시켜주는 것으로 동기화를 이해하고 있다.
→ 공유 영역에 동시에 접근함으로 인해 발생하는 불일치를 막기 위해 순차적으로 공유 영역을 수행하도록 보장하는 메커니즘이다.
흔히 공유하는 어떤 자원을 다른 기기에서 사용하고 현재 사용하려는 기기에 아직 적용이 되어 있지 않아, 동기화가 완료된 후 이어서 작업한 경험이 있다.
이 경험을 살려서 이해해보자.
→ 모든 기계 명령은 원자성을 가진다.
int one = 1;
one += 1
다음과 같은 명령어가 존재할 때,
여기서
one += 1;
이 명령어를 기계어로 표시하면 다음과 같은 절차를 따른다.
이런 간단한 명령어도 기계어로 변환되어 CPU가 인식할 때 다음의 3가지 명령으로 분리된다.
이 각각의 명령의 수행 중에는 인터럽트를 받지 않아 원자성을 보장 받는다.
하지만 이러한 명령어가 3개로 나눠져 있기 때문에 1번을 실행이 종료되고 나서 동일한 Thread가 2번을 실행할 수 있고, 다른 Thread가 1번을 실행할 수 있다.
즉, 고급 언어로 작성된 명령어 하나는 여러 개의 기계어 명령어로 해석될 수 있고, 이로 인해 CPU 연산으로 원자성을 보장 받지 못하는 일이 발생할 수 있다.
private final static int GOAL = 100000;
private static int result = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread_01 = new Thread(()->{
for(int i = 0 ; i<GOAL; i++){
result++;
}
});
Thread thread_02 = new Thread(()->{
for(int i = 0 ; i<GOAL; i++){
result++;
}
});
thread_01.start();
thread_02.start();
thread_01.join();
thread_02.join();
System.out.println("PREDICT : "+200000);
System.out.println("RESULT : "+result);
}
→ 두 Thread는 10만이 될 때까지, 공유 자원 result에 1씩 더하는 연산을 진행한다. 각기 다른 두 Thread에서 공유 자원을 사용하기 때문에 결과는 200,000이 되어야 할 것 같지만 다음과 같은 일이 발생한다.
- Thread 1에서 result 값을 100으로 할당한다.
- 이 때 CPU가 Thread 2로 할당된다 (Context Switching).
- Thread 2에서 result 값을 99로 할당하고 다시 CPU가 Thread 1으로 할당된다.
- Thread 1이 result 값을 100으로 저장했다. 하지만 그 이후 Thread 2가 result 값을 다시 99로 저장해 결국 result 값은 99가 된다.
이제 조금 더 세분화해서 알아보자.
→ 위의 코드에서 공유 자원인 result 를 조작하여 데이터 불일치 문제를 발생시키는 코드 영역을 말한다.
result++;
이 영역에 대한 원자성을 확보해야하며,
이를 위해서 고려해야하는 충족요건들에 대해서 알아보자.
→ 한 Thread 가 임계 영역을 실행하고 있을때, 발생할 수 있는 문제점이다.
해당 문제를 해결하기 위해서는 다음의 3가지 조건이 요구 된다.
어떤 Thread 가 임계 영역에서 실행 중이면, 다른 스레드는 임계영역에 접근할 수 없다.
- 임계 영역에 작업중인 스레드가 없을때, 동시에 스레드가 임계영역에 접근하려는 경우
어떤 스레드가 들어갈 것인지 선택해주는 매커니즘이 존재해야한다.
- 대기중인 Thread가 무한정 대기하는 것이 아닌 임계영역을 수행할 수 있도록 매커니즘이 구성되어야한다.
→ 기아상태 방지 ( Thread 가 계속 대기 상태 = 기아 상태 )
이와 같은 문제점을 고려하여 작성하면 동기화 문제를 해결할 수 있다.
자바에서 이를 고려해서 동기화 메커니즘을 구성하면 다음과 같이 변경할 수 있다.
ublic class Part02_Thread_Sync_Apply {
private final static int GOAL = 100000;
private static int result = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread_01 = new Thread(()->{
for(int i = 0 ; i<GOAL; i++){
// ** 입장영역 : 임계 영역에 들어가기전, 임계영역 진입이 가능한지 확인 하는 영역
synchronized (Part02_Thread_Sync_Apply.class) { // ** 임계 영역
result++;
}
// ** 임계 영역 -- 하나의 스레드만 접근하여 공유자원을 조작할 수 있는 영역, 동기화 메커니즘을 구성
// ** 퇴장 영역 : 임계영역을 빠져나왔음을 알리는 영역, 이후 다른 스레드들이 임계영역에 접근이 가능해진다.
}
// remainder 영역 : 이외 나머지 영역
});
Thread thread_02 = new Thread(()->{
for(int i = 0 ; i<GOAL; i++){
synchronized (Part02_Thread_Sync_Apply.class) {
result++;
}
}
});
thread_01.start();
thread_02.start();
thread_01.join();
thread_02.join();
System.out.println("PREDICT : "+200000);
System.out.println("RESULT : "+result);
}
자바에서 사용하는 예약어인 synchronized를 사용하여 임계 영역을 설정하고,
위의 3가지 조건을 충족시켜 Thread 1개만 접근이 가능하도록 하여
원하는 값을 얻을 수 있다.