⬛ 출금 예제로 보는 동시성 문제
공유 자원: 여러 스레드가 접근하는 자원
쓰레드 두개를 만들어 동시에 잔고가 천원인 계좌에서 동시에 돈을 출금하는 상황을 만들어보자.
public class BankAccountV1 implements BankAccount {
private int balance;
//volatile private int balance;
public BankAccountV1(int initialBalance) {
this.balance = initialBalance;
}
@Override
public boolean withdraw(int amount) {
log("거래 시작: " + getClass().getSimpleName());
log("[검증 시작] 출금액: " + amount + ", 잔액: " + balance);
if (balance < amount) {
log("[검증 실패] 출금액: " + amount + ", 잔액: " + balance);
return false;
}
log("[검증 완료] 출금액: " + amount + ", 잔액: " + balance); sleep(1000); // 출금에 걸리는 시간으로 가정
balance = balance - amount;
log("[출금 완료] 출금액: " + amount + ", 변경 잔액: " + balance);
log("거래 종료");
return true;
}
@Override
public int getBalance() {
return balance;
}
}
public class WithdrawTask implements Runnable {
private BankAccount account;
private int amount;
public WithdrawTask(BankAccount account, int amount) {
this.account = account;
this.amount = amount;
}
@Override
public void run() {
account.withdraw(amount);
}
}
public class BankMain {
public static void main(String[] args) throws InterruptedException {
BankAccount account = new BankAccountV1(1000);
Thread t1 = new Thread(new WithdrawTask(account, 800), "t1");
Thread t2 = new Thread(new WithdrawTask(account, 800), "t2");
t1.start();
t2.start();
sleep(500); // 검증 완료까지 잠시 대기
log("t1 state: " + t1.getState());
log("t2 state: " + t2.getState());
t1.join();
t2.join();
log("최종 잔액: " + account.getBalance());
}
}
두 스레드는 같은 계좌(x001) 인스턴스를 참조하며,따라서 같은 계좌에서 출금을 시도한다.
예상과 달리 두번다 출금이되고, -600원이 되었다...!
그렇다면 왜 이런 문제가 발생할까? 아래 그림처럼 t1이 sleep(1000)을 처리중이고, t2가 검증 로직인 상황을 보자.
아직 잔액(balance)을 줄이지 못했기 때문에 t2는 검증로직에서 현재 잔액을 1000원으로 확인한다.
만약 동시 실행이 된다해도 문제가 된다. 둘다 1000-800을 실행하므로 200원이 잔액으로 계산된다.
⬛ 임계 영역
위의 문제가 생긴 근본 원인은, 결국 여러 스레드가 함께 사용하는 공유 자원을 여러 단계로 나누어 사용하기 때문이다.
아래에서의 가정은 검증~출금 단계까지 잔액(balance)이 같은 금액으로 유지되어야 한다는 것이다.
출금() {
1. 검증 단계: 잔액(balance) 확인
2. 출금 단계: 잔액(balance) 감소
}
🟢 임계 영역 (critical section)
- 여러 스레드가 동시에 접근하면 데이터 불일치나 예상치 못한 동작이 발생할 수 있는 위험하고 또 중요한 코드 부분을 뜻한다.
- 여러 스레드가 동시에 접근해서는 안 되는 공유 자원을 접근하거나 수정하는 부분을 의미한다.
- 예) 공유 변수나 공유 객체를 수정
이러한 임계 영역은 한 번에 하나의 스레드만 접근할 수 있도록 보호해야한다.
이를위해 synchronized 키워드를 통해 간단히 임계 영역을 보호할 수 있다.
🟢 synchronized 동작방식
모든 객체는 내부에 자신만의 lock을 가지고 있는데, 위 키워드가 있는 메서드에 진입하려면 반드시 해당 인스턴스의 락이 있어야한다.
- 락이 없으면 해당 스레드는 락을 획득할 때까지 BLOCKED 상태로 대기한다.
- 락이 반납되면 락 획득을 대기하는 스레드는 자동으로 락을 획득한다.
- 락을 획득하는 순서는 보장되지 않는다.
위의 withdraw(),getBalance()에 synchronized 키워드를 추가하면, 이제 한 번에 하나의 스레드만 실행할 수 있다.
⬛ synchronized 코드 블럭
synchronized는 한 번에 하나의 스레드만 실행할 수 있기때문에, 성능이 떨어질 수 있다.
따라서 임계구역을 최대한 필요한 곳으로 한정해서 설정해야한다.
자바는 이를 위해 메서드 단위가 아니라, 특정 코드 블럭에 synchronized를 적용할 수 있도록한다.
synchronized (this) {...}
⬛ synchronized 동기화 정리
🟢 자바 동기화(synchronization)
- 여러 스레드가 동시에 접근할 수 있는 자원에 대해 일관성 있고 안전한 접근을 보장하기 위한 메커니즘
🟢 동기화로 해결 가능한 문제들
- 경합 조건(condition): 두 개 이상의 스레드가 경쟁적으로 동일한 자원을 수정할 때 발생하는 문제.
- 데이터 일관성: 여러 스레드가 동시에 읽고 쓰는 데이터의 일관성을 유지.
참고로, 지역 변수는 각 스레드의 개별 공간인 스택 영역에 생기기 때문에, 지역 변수는 절대로! 다른 스레드와 공유되지 않는다.
(지역 변수는 동기화에 대한 걱정을 하지 않아도된다!)
또한 공유자원에 final을 붙이면 어떤 스레드도 값을 변경할 수 없기때문에, 멀티스레드 상황에 문제 없는 안전한 공유 자원이 된다.
synchronized는 자동 잠금 해제(lock을 대기중인 다른 스레드에 넘김)로 사용이 매우 편리하지만, 스레드 공정성문제와 BLOCKED상태 스레드는 락이 풀릴때까지 무한 대기한다는 단점이있다.
따라서 이를 위해 더 세밀한 제어가 가능한 java.util.concurrent 패키지가 추가되었다.
🌼 추가 보충 내용
- 질문: synchronized 키워드를 같은 변수 count에 사용하는 두 메서드(increment와 getCount)가 있을 때, 한 스레드가 increment를 호출하면 다른 스레드가 getCount에 접근할 수 있나?없나?
static class Counter {
private int count = 0;
public synchronized void increment() {
count = count + 1;
}
public synchronized int getCount() {
return count;
}
}
- 답변: synchronized 키워드를 사용한 메서드들은 객체의 모니터 락을 사용하여 동기화된다.
- 한 스레드가 increment() 메서드를 실행 중이라면, 이 스레드는 Counter 객체의 모니터 락을 소유하게 된다.
- 다른 스레드는 getCount() 메서드를 호출할 수 없으며, increment() 메서드가 종료되어 락이 해제될 때까지 기다려야 한다.
- 따라서, 동일한 객체에서 increment()와 getCount() 메서드는 동시에 실행되지 않는다. 하나의 스레드가 increment()를 실행 중이면 다른 스레드는 getCount()에 접근할 수 없다.
'Java' 카테고리의 다른 글
[Java] 김영한의 실전 자바 - 고급 1편 섹션8 생산자 소비자 문제1 (0) | 2024.09.20 |
---|---|
[Java] 김영한의 실전 자바 - 고급 1편 섹션7 고급 동기화 - concurrent.Lock (4) | 2024.08.28 |
[Java] 김영한의 실전 자바 - 고급 1편, 멀티스레드와 동시성 섹션2,3 스레드 제어와 생명주기 (0) | 2024.07.28 |
[Java의 정석] chapter13 쓰레드 thread 알아보기 (0) | 2024.07.18 |
[Java] 김영한의 실전 자바 - 고급 1편, 멀티스레드와 동시성 섹션1 프로세스와 스레드 소개 (0) | 2024.07.16 |