# 동기화
프로세스와 스레드들은 동시 다발적으로 여러개가 서로 협력하고 영향을 주고 받으며 실행된다.
이때, 프로세스나 스레드들이 서로 협력하고 영향을 주고 받는데 사용하는 자원을 `공유자원`이라고 한다. 예를 들어 파일, 데이터베이스, 네트워크, 메모리, 프린터 등이 있다.
만약 이 공유자원에 아무런 제어없이 여러 프로세스나 스레드가 동시에 접근하려고 하면 문제가 발생할 수 있다. 이러한 문제를 해결하기 위해 `동기화`가 필요하다.
## 동기화 문제 예시 코드
```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` 연산인 경우에도, 코드상으로는 한 번에 수행되는 것 처럼 보이지만 컴파일 후에는 다음과 여러 단계로 나누어진다.

만약 `a`라는 값이 공유자원이고, 이렇게 여러 단계로 나누어진 연산이 동시에 여러 스레드에서 수행되면 문제가 발생할 수 있다.

이렇게 여러 프로세스/스레드가 동시에 공유자원에 접근하여 예상치 못한 결과가 발생하는 상태를 `경쟁 상태`이라고 한다.
## 경쟁상태 해결 방법

경쟁상태를 해결하기 위해서는 `공유자원`에 대한 접근을 `동기화`하여 한 번에 하나의 프로세스만 접근할 수 있도록 하는 코드 영역을 만들어야 한다.
이렇게 한 번에 하나의 프로세스만 접근할 수 있도록 하는 코드 영역을 `임계 구역`이라고 한다.

이 임계구역을 만들기 위해 뮤텍스, 세마포어, 모니터 등의 동기화 기법을 사용할 수 있다.
## 동기화 기법 : 뮤텍스 (Mutex)

- 뮤텍스(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)

- 공유 자원이 여러 개일 때 사용하는 동기화 기법이다
- 세마포어는 허용할 수 있는 최대 동시 접근 수를 나타내는 카운터로, 다수의 스레드가 공유 자원에 접근할 수 있도록 허용한다
- 세마포어는 동시에 접근 가능한 스레드의 개수를 지정할 수 있다. 세마포어 값이 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)

- 모니터는 고수준 동기화 메커니즘이다.
- 모니터는 `모니터 큐` 를 통해 순차적으로 하나의 스레드만이 임계 영역에 진입할 수 있도록 한다.
- 모니터는 뮤텍스와 세마포어와 달리 **공유 자원에 대한 접근을 숨기고** 해당 접근에 대해 인터페이스만 제공한다.
- 모니터는 매번 임계구역 앞 뒤로 락을 걸어주고 풀어주는 번거로움을 해결하기 위한 방법으로 사용자가 다루기 편한, 상호배제와 실행 순서를 위한 두 경우 모두를 고려하는 동기화 도구이다
```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) {
// 동기화된 코드 블록
}
```