스레드 03. 동기화(Synchronization)
3. 동기화(Synchronization)
동기화는 멀티스레딩 환경에서 여러 스레드가 공유 자원에 안전하게 접근할 수 있도록 하는 기법입니다. 스레드들이 동시에 같은 데이터나 자원에 접근하면 데이터 불일치나 충돌이 발생할 수 있습니다. 이런 문제를 해결하기 위해 동기화(Synchronization)가 필요합니다.
3.1 동기화의 필요성
멀티스레드 프로그램에서는 여러 스레드가 동시에 같은 자원(예: 변수, 객체, 파일 등)에 접근할 수 있기 때문에 문제가 발생할 수 있습니다. 이러한 자원에 여러 스레드가 동시에 접근하여 읽거나 수정할 경우 레이스 컨디션(Race Condition)과 같은 문제가 발생할 수 있습니다.
레이스 컨디션은 동시에 여러 스레드가 자원에 접근하면서 결과가 예측할 수 없게 되는 상황을 말합니다. 예를 들어, 두 스레드가 동시에 같은 변수를 수정하려고 할 때, 어느 스레드가 먼저 실행될지 알 수 없기 때문에 의도한 결과가 나오지 않을 수 있습니다.
이를 해결하기 위해서는 스레드 간 동기화(Synchronization)가 필요합니다. 동기화는 여러 스레드가 동시에 공유 자원에 접근할 때, 특정 스레드만 접근할 수 있도록 제어하여 데이터의 일관성과 정확성을 보장합니다.
3.2 동기화 문제의 예시
public class Counter {
private int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
위 예시에서 increment()
메서드를 여러 스레드가 동시에 호출하면 문제가 발생할 수 있습니다. 두 개의 스레드가 동시에 count++
를 실행하려 할 때, 한 스레드가 값을 읽은 후 아직 값을 저장하지 않았을 때 다른 스레드가 같은 값을 읽는 일이 생길 수 있습니다. 결과적으로 count
값이 의도보다 적게 증가할 수 있습니다. 이러한 문제를 방지하기 위해 동기화가 필요합니다.
3.3 동기화의 기본 기법
동기화는 여러 방법으로 구현될 수 있으며, 이를 통해 스레드 간 자원 충돌을 방지하고 일관성을 유지할 수 있습니다.
3.3.1 뮤텍스(Mutex)
뮤텍스(Mutex)는 Mutual Exclusion(상호 배제)의 줄임말로, 한 번에 하나의 스레드만 공유 자원에 접근할 수 있도록 제한하는 메커니즘입니다. 뮤텍스는 자원의 "잠금"과 "해제" 상태를 관리하며, 다른 스레드가 해당 자원을 잠근 상태에서는 접근할 수 없습니다.
public class Counter {
private int count = 0;
// synchronized 키워드를 사용하여 동기화 처리
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
위 코드에서는 synchronized
키워드를 사용하여 increment()
메서드를 동기화했습니다. 이제 여러 스레드가 이 메서드를 호출하더라도, 하나의 스레드만 자원에 접근하여 count
값을 안전하게 증가시킬 수 있습니다. 다른 스레드들은 첫 번째 스레드가 작업을 끝낼 때까지 대기하게 됩니다.
3.3.2 락(Lock)
락(Lock)은 뮤텍스와 유사한 방식으로, 특정 자원을 보호하기 위한 동기화 메커니즘입니다. 자원을 사용하려는 스레드는 락을 얻어야 하고, 작업이 끝나면 락을 반환해야 합니다. 자바에서는 ReentrantLock
클래스를 사용하여 락을 구현할 수 있습니다.
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private int count = 0;
private ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock(); // 락을 얻음
try {
count++;
} finally {
lock.unlock(); // 락을 해제
}
}
public int getCount() {
return count;
}
}
ReentrantLock
을 사용하면 synchronized
와 비슷한 방식으로 동기화를 구현할 수 있지만, 락을 명시적으로 제어할 수 있다는 장점이 있습니다. 예를 들어, 락을 여러 번 얻고 해제할 수 있는 기능이나, 조건에 맞게 락을 해제하는 기능을 추가할 수 있습니다.
3.3.3 세마포어(Semaphore)
세마포어(Semaphore)는 뮤텍스와 비슷하지만, 한 번에 여러 스레드가 동시에 접근할 수 있는 자원을 제한하는 데 사용됩니다. 세마포어는 허용된 스레드 수를 지정하고, 그 수를 초과하는 스레드는 대기하게 만듭니다.
import java.util.concurrent.Semaphore;
public class Resource {
private Semaphore semaphore = new Semaphore(3); // 동시에 3개의 스레드만 접근 가능
public void accessResource() throws InterruptedException {
semaphore.acquire(); // 세마포어 획득
try {
// 자원에 접근하는 코드
} finally {
semaphore.release(); // 세마포어 해제
}
}
}
위 코드에서는 Semaphore
를 사용하여 동시에 최대 3개의 스레드만 자원에 접근할 수 있도록 설정했습니다. 세마포어는 자원을 병렬로 처리해야 하지만, 동시에 접근할 수 있는 스레드 수를 제한하고 싶을 때 유용합니다.
3.3.4 모니터(Monitor)
모니터는 객체 수준에서 스레드 간 동기화를 관리하는 메커니즘입니다. 자바에서 synchronized
키워드는 내부적으로 모니터를 사용해 스레드 간 동기화를 처리합니다. 모니터는 한 번에 하나의 스레드만 특정 블록이나 메서드에 진입할 수 있도록 합니다.
3.4 동기화 문제
3.4.1 데드락(Deadlock)
데드락은 두 개 이상의 스레드가 서로의 자원을 기다리면서 영원히 멈추는 상태를 말합니다. 예를 들어, 스레드 A가 자원 1을 가지고 있고, 자원 2를 기다리는 동안, 스레드 B는 자원 2를 가지고 자원 1을 기다린다면 둘 다 영원히 작업을 진행할 수 없습니다.
3.4.2 레이스 컨디션(Race Condition)
레이스 컨디션은 여러 스레드가 동시에 공유 자원에 접근할 때 발생하는 예측 불가능한 동작을 의미합니다. 예를 들어, 두 스레드가 동시에 변수 값을 읽고, 변경된 값을 저장하려고 할 때, 어느 스레드가 먼저 실행될지 알 수 없어 예기치 않은 결과가 발생할 수 있습니다.
3.5 동기화의 올바른 사용
- 과도한 동기화 피하기: 불필요하게 동기화를 많이 사용하면 성능 저하가 발생할 수 있습니다. 가능한 최소한의 코드 블록에서만 동기화를 사용해야 합니다.
- 적절한 락 관리: 락을 사용하면 명시적으로 자원을 보호할 수 있지만, 락을 해제하지 않으면 데드락이 발생할 수 있으므로 락 해제를 반드시 보장해야 합니다. 자바에서는
finally
블록을 사용하여 락을 안전하게 해제할 수 있습니다.
3.6 동기화 전략
동기화 전략을 선택할 때는 어떤 자원을 보호해야 하는지, 스레드가 자원을 어떻게 사용하는지에 따라 적절한 방법을 사용해야 합니다. 자바에서는 synchronized
, ReentrantLock
, Semaphore
등의 동기화 도구를 적절히 조합하여 사용할 수 있습니다.