# 동기화 프로세스와 스레드들은 동시 다발적으로 여러개가 서로 협력하고 영향을 주고 받으며 실행된다. 이때, 프로세스나 스레드들이 서로 협력하고 영향을 주고 받는데 사용하는 자원을 `공유자원`이라고 한다. 예를 들어 파일, 데이터베이스, 네트워크, 메모리, 프린터 등이 있다. 만약 이 공유자원에 아무런 제어없이 여러 프로세스나 스레드가 동시에 접근하려고 하면 문제가 발생할 수 있다. 이러한 문제를 해결하기 위해 `동기화`가 필요하다. ## 동기화 문제 예시 코드 ```java class BankAccount { private int balance = 0; public int getBalance() { return balance; } public void withdraw(int amount) { balance = balance - amount; } public void deposit(int amount) { balance = balance + amount; } } class WithdrawThread extends Thread { private BankAccount account; public WithdrawThread(BankAccount account) { this.account = account; } public void run() { for (int i = 0; i < 1000; i++) { account.withdraw(1); } } } class DepositThread extends Thread { private BankAccount account; public DepositThread(BankAccount account) { this.account = account; } public void run() { for (int i = 0; i < 1000; i++) { account.deposit(1); } } } public class Main { public static void main(String[] args) throws InterruptedException { BankAccount account = new BankAccount(); WithdrawThread withdrawThread = new WithdrawThread(account); DepositThread depositThread = new DepositThread(account); withdrawThread.start(); depositThread.start(); withdrawThread.join(); depositThread.join(); System.out.println("잔액: " + account.getBalance()); } } ``` 위 코드는 은행 계좌를 나타내는 `BankAccount` 클래스와 이 계좌를 사용하는 `WithdrawThread`와 `DepositThread` 클래스를 가지고 있다. - `WithdrawThread`는 계좌에서 1원을 1000번 출금하는 일을 한다. - `DepositThread`는 계좌에 1원을 1000번 입금하는 일을 한다. 위 코드는 `잔액`을 출력했을 때 0이 나와야 하는데, 그렇지 않은 경우가 발생할 수 있다. 이는 `동기화 문제`로 인해 발생하는 문제이다. ## 동기화 문제가 발생하는 이유 동기화 문제가 발생하는 이유는 원자적으로 수행되지 않는 연산 때문이다. (원자적 : 연산이 중간에 중단되지 않고 완전히 수행되는 을 의미) 보통 연산을 할 때는 `읽기`, `연산`, `쓰기`의 세 단계로 나누어진다. 그리고 `a = a + 1` 연산인 경우에도, 코드상으로는 한 번에 수행되는 것 처럼 보이지만 컴파일 후에는 다음과 여러 단계로 나누어진다. ![](https://i.imgur.com/BzjHnzm.png) 만약 `a`라는 값이 공유자원이고, 이렇게 여러 단계로 나누어진 연산이 동시에 여러 스레드에서 수행되면 문제가 발생할 수 있다. ![](https://i.imgur.com/2ZIxHfP.png) 이렇게 여러 프로세스/스레드가 동시에 공유자원에 접근하여 예상치 못한 결과가 발생하는 상태를 `경쟁 상태`이라고 한다. ## 경쟁상태 해결 방법 ![image](https://hackmd.io/_uploads/r1WEMXjTp.png) 경쟁상태를 해결하기 위해서는 `공유자원`에 대한 접근을 `동기화`하여 한 번에 하나의 프로세스만 접근할 수 있도록 하는 코드 영역을 만들어야 한다. 이렇게 한 번에 하나의 프로세스만 접근할 수 있도록 하는 코드 영역을 `임계 구역`이라고 한다. ![image](https://hackmd.io/_uploads/rydwEVoTp.png) 이 임계구역을 만들기 위해 뮤텍스, 세마포어, 모니터 등의 동기화 기법을 사용할 수 있다. ## 동기화 기법 : 뮤텍스 (Mutex) ![image](https://hackmd.io/_uploads/r1MosNjpT.png) - 뮤텍스(Mutex)는 상호 배제(Mutual Exclusion)의 줄임말이다 - 뮤텍스는 공유 자원에 대한 접근을 조정하여, **한 번에 하나의 스레드만이 공유 자원을 사용할 수 있도록 한다** - 임계 영역에 진입하기 전에 락(lock)을 획득하고, 임계 영역을 빠져나올 때 락을 해제하여 다른 스레드들이 접근할 수 있도록 한다. - ex) 탈의실에 들어가기 전에 문을 잠그고, 나올 때 문을 열어주는 것과 비슷한 개념 - 뮤텍스는 일종의 바이너리 세마포어로 볼 수 있다 - 뮤텍스는 lock()과 unlock() 두 가지 주요 연산으로 구성된다. 1. 어떤 스레드가 공유 자원을 사용하기 전에 lock()을 호출하여 뮤텍스를 획득한다. 2. 이 시점부터 다른 어떤 스레드도 이 뮤텍스를 획득할 수 없다. 3. 자원 사용이 끝나면 unlock()을 호출하여 뮤텍스를 반환한다. 4. 이제 다른 스레드가 뮤텍스를 획득하고 자원을 사용할 수 있다. ```java import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; class BankAccount { private int balance = 0; private final Lock lock = new ReentrantLock(); public int getBalance() { lock.lock(); try { return balance; } finally { lock.unlock(); } } public void withdraw(int amount) { lock.lock(); try { balance = balance - amount; } finally { lock.unlock(); } } public void deposit(int amount) { lock.lock(); try { balance = balance + amount; } finally { lock.unlock(); } } } ``` ### 뮤텍스의 특징 뮤텍스의 장점은 단순성과 명확성에 있다. 뮤텍스는 공유 자원에 대한 접근을 명확하게 제어한다. 그러나 뮤텍스의 사용이 부적절하면 교착 상태(deadlock)나 경쟁 상태(race condition)와 같은 문제를 일으킬 수 있다. ## 동기화 기법 : 세마포어 (Semaphore) ![image](https://hackmd.io/_uploads/H1_jj4jaT.png) - 공유 자원이 여러 개일 때 사용하는 동기화 기법이다 - 세마포어는 허용할 수 있는 최대 동시 접근 수를 나타내는 카운터로, 다수의 스레드가 공유 자원에 접근할 수 있도록 허용한다 - 세마포어는 동시에 접근 가능한 스레드의 개수를 지정할 수 있다. 세마포어 값이 1이면 뮤텍스와 동일한 역할을 하며, 값이 2 이상이면 동시에 접근 가능한 스레드의 수를 제어할 수 있다. 스레드가 임계 영역에 진입하기 전에 세마포어 값을 확인하고, 값이 허용된 범위 내에 있을 때만 락을 획득할 수 있는 형식이다. - 뮤텍스 상위 호환 이라고 보면 된다. ```java import java.util.concurrent.Semaphore; class BankAccount { private int balance = 0; private Semaphore semaphore = new Semaphore(1); public int getBalance() { return balance; } public void withdraw(int amount) { try { semaphore.acquire(); balance = balance - amount; } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { semaphore.release(); } } public void deposit(int amount) { try { semaphore.acquire(); balance = balance + amount; } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { semaphore.release(); } } } ``` ### 세마포어의 특징 세마포어는 다수의 스레드가 동시에 자원을 사용할 수 있게 해준다는 점에서 뮤텍스와 차별화된다. 하지만 세마포어를 사용할 때는 세심한 주의가 필요하며, 잘못 사용하면 시스템의 복잡성이 증가하고 오류를 일으킬 확률이 높아진다. ## 동기화 기법 : 모니터 (Monitor) ![image](https://hackmd.io/_uploads/S1hos4opp.png) - 모니터는 고수준 동기화 메커니즘이다. - 모니터는 `모니터 큐` 를 통해 순차적으로 하나의 스레드만이 임계 영역에 진입할 수 있도록 한다. - 모니터는 뮤텍스와 세마포어와 달리 **공유 자원에 대한 접근을 숨기고** 해당 접근에 대해 인터페이스만 제공한다. - 모니터는 매번 임계구역 앞 뒤로 락을 걸어주고 풀어주는 번거로움을 해결하기 위한 방법으로 사용자가 다루기 편한, 상호배제와 실행 순서를 위한 두 경우 모두를 고려하는 동기화 도구이다 ```java class BankAccount { private int balance = 0; public synchronized int getBalance() { return balance; } public synchronized void withdraw(int amount) { balance = balance - amount; } public synchronized void deposit(int amount) { balance = balance + amount; } } ``` ### 모니터의 특징 모니터의 장점은 사용의 간편함과 안전성에 있다. 자동 잠금 메커니즘 덕분에 **프로그래머가 직접 잠금을 관리할 필요가 없다**. 그러나 모니터는 다양한 프로그래밍 언어에서 지원되지 않을 수 있으므로 사용 환경을 잘 확인해야 한다. ### 자바의 `syncronized` - 자바의 `syncronized` 키워드는 멀티스레드 환경에서 공유 자원에 대한 동시 접근을 제어하기 위한 동기화 메커니즘을 제공한다 - `synchronized`를 사용하면 한 스레드가 공유 자원에 접근하는 동안 다른 스레드는 해당 자원에 접근할 수 없다. - 자바는 `synchronized` 키워드를 통해 모니터를 사용할 수 있다. - `synchronized` 키워드는 다음 두 가지 방식으로 사용할 수 있다. - 동기화 메서드 - 동기화 블록 #### 동기화 메서드 - 메서드 선언부에 synchronized 키워드를 추가함으로써 해당 메서드를 동기화하는 방법 - 이 경우, 해당 메서드에 대한 동기화는 객체 인스턴스에 대해 수행된다. - 따라서 한 스레드가 동기화된 메서드를 실행하는 동안 해당 객체 인스턴스의 다른 동기화된 메서드에는 다른 스레드가 접근할 수 없다 ``` java public synchronized void myMethod() { // 동기화된 코드 블록 } ``` #### 동기화 블록 - `synchronized` 키워드를 사용하여 특정 코드 블록만 동기화할 수도 있다. - 동기화 블록을 사용할 때는 동기화를 수행할 객체를 명시해야 한다. - 한 스레드가 동기화된 블록에 접근하면, 명시된 객체에 대한 잠금이 이루어지며, 다른 스레드는 해당 객체의 다른 동기화된 블록에 접근할 수 없다. ``` java synchronized (myObject) { // 동기화된 코드 블록 } ```