동기화란
스레드 개념의 도입으로 하나의 프로세스에서 여러 스레드들이 동시에 작업을 처리할 수 있게 되었지만, 스레드들이 프로세스의 자원을 공유하며 Race Condition이나 데이터 불일치와 같은 동시성 문제들이 발생하게 되었다.
여러 스레드가 접근할 때 동시성 문제가 발생할 수 있는 공유 자원을 임계 영역이라 하며, 동시성 문제가 발생하지 않으려면 임계 영역에 하나의 스레드만 접근할 수 있도록 해야한다.
스레드 간의 수행 시기를 맞추어 동시성 문제를 예방하는 것을 동기화라고 하며,자바에서도 멀티 스레드 상황에서 동시성 문제를 해결할 수 있는 여러 동기화 방법들을 제공한다.
synchronized의 단점
synchronized는 자바에서 동기화를 위한 가장 대표적인 키워드이다.
자바에서 모든 객체는 모니터 락을 갖고있으며, synchronized는 객체의 모니터 락을 이용한다.
public class BankAccount {
private int balance;
public BankAccount(int initialBalance) {
this.balance = initialBalance;
}
//출금 로직
@Override
public boolean withdraw(int amount) {
//임계영역 진입 -> 스레드 BLOCKED 상태 변경
synchronized (this) {
if (balance < amount) {
return false;
}
// 출금에 걸리는 시간으로 가정
sleep(1000);
balance = balance - amount;
}
return true;
}
//잔액 조회
@Override
public synchronized int getBalance() {
return balance;
}
}
===
// 계좌 - 임계 영역
BankAccount account = new BankAccount(1000);
// 하나의 계좌에 대해서, 스레드 1,2가 출금 태스크 실행
Thread t1 = new Thread(new WithdrawTask(account, 800), "t1");
Thread t2 = new Thread(new WithdrawTask(account, 800), "t2");
t1.start();
t2.start();
sleep(500); // 검증 완료까지 잠시 대기
스레드1이 synchronized 영역에 접근하여 모니터 락을 획득한 이후 스레드 2가 synchronized 영역에 접근하려 하면, 스레드 2는 락을 획득하기 위해 BLOCKED 상태가 되어 JVM 레벨에서 관리되며 대기하게 된다.
이 때 문제는 BLOCKED 상태의 스레드가 JVM 레벨에서 관리되기 때문에 관리자가 직접 제어할 수 없다는 것이다.
인터럽트에도 응답하지 않는다. 단지 락을 획득할 때까지 대기하게 되며, 무한 대기 상태에 빠질 수도 있다.
또한 BLOCKED 상태의 스레드들 중 어떤 스레드가 락을 획득할 지 알 수 없는 공정성 문제도 있다.
스레드 2가 많이 기다렸다고 하더라도 뒤늦게 synchronized 영역에 접근하여 락 획득을 시도한 스레드 3이 락을 획득할 수도 있다.
ReentrantLock 개요
ReentrantLock은 자바에서 제공한 Lock 인터페이스의 구현체로, synchronized의 단점을 보완한 동기화 도구이다.
public class BankAccount {
private int balance;
//ReentrantLock
private final Lock lock = new ReentrantLock();
public BankAccountV6(int initialBalance) {
this.balance = initialBalance;
}
//출금 로직
@Override
public boolean withdraw(int amount) {
try {
//lock 획득 시도
//lock 획득 대기 시, WAITING 상태 변경
if (!lock.tryLock(500, TimeUnit.MILLISECONDS)) {
return false;
}
//인터럽트의 영향을 받을 수 있음
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
try {
if (balance < amount) {
return false;
}
// 출금에 걸리는 시간으로 가정
sleep(1000);
balance = balance - amount;
} finally {
//lock 획득 해제
lock.unlock();
}
log("거래 종료");
return true;
}
//잔액 조회
@Override
public int getBalance() {
lock.lock(); // ReentrantLock 이용하여 lock을 걸기
try {
return balance;
} finally {
lock.unlock(); // ReentrantLock 이용하여 lock 해제
}
}
}
synchronized는 low level의 모니터 락을 이용하지만, ReentrantLock의 락은 자바에서 제공하는 기능이다.
코드를 통해 락을 직접 획득하고 해제하는 것을 볼 수 있다.
synchronized와 다르게 락의 획득을 기다리는 스레드는 WAITING 상태로 변경되며, 인터럽트의 영향을 받을 수 있다.
ReentrantLock 기능
ReentrantLock의 세부 기능을 살펴보자.
void lock()
- 락을 획득한다.
- 만약 다른 스레드가 이미 락을 획득했다면, 락을 획득할 때까지 대기한다.
- 인터럽트의 응답하지 않는다.
void lockInterruptibly()
- lock()과 동일하다.
- 인터럽트의 응답한다.
boolean tryLock()
- 락 획득을 시도한다.
- 락을 획득하면 true, 획득하지 못하면 false를 반환한다.
boolean tryLock(long time, TimeUnit unit)
- tryLock()의 제한시간이 추가되었다.
- 인터럽트의 응답한다.
boolean isHeldByCurrentThread()
- 현재 스레드가 해당 락을 갖고있는지 확인한다.
void unlock()
- 락을 해제한다.
공정성
synchronized를 통한 동기화 방식은 BLOCKED 상태의 스레드가 여러 개일 경우 어떤 스레드가 락을 획득할 지 알 수 없다.
최악의 경우 특정 스레드는 너무 오랜기간 락을 획득하지 못할 수 있다.
// 공정 모드 락
private final Lock fairLock = new ReentrantLock(true);
공정성 문제를 해결하기 위해, ReentranLock은 락을 요청한 순서대로 스레드가 락을 획득하는 공정 모드를 지원한다.
대기 큐에서 먼저 대기한 스레드가 락을 먼저 획득할 수 있게하여 기아 현상을 방지한다. 순서를 보장하기 때문에 속도는 비공정 모드보다 비교적 느릴 수 있다.
주의 사항
락을 획득하여 작업을 마무리한 후, 반드시 unlock() 메서드를 통해 락을 해제해주어야 한다. 그렇지 않으면 다른 스레드들은 무한 대기 상태에 빠지게 된다.
synchronized는 이러한 부분을 자동으로 해제해주지만, ReentrantLock은 명시적으로 해제해주어야 하며 이것이 synchronized와 크게 다른점이며 유일한 단점이 되겠다.
'Language > Java' 카테고리의 다른 글
| [Java] Callable과 Future & CompletableFuture (0) | 2026.02.01 |
|---|---|
| [Java] 자바 스레드 풀과 ExecutorService (0) | 2026.01.27 |
| [Java] 자바 스레드 생명 주기와 메모리 가시성 (0) | 2025.12.30 |
| [Java] Stream Collector, Downstream Collector (0) | 2025.12.26 |
| [Java] Stream API와 지연 연산 (0) | 2025.12.25 |