


- BankAccountV1와 같은데, withdraw() , getBalance() 코드에 synchronized 키워드가 추가되었다.
- 이제 withdraw() , getBalance() 메서드는 한 번에 하나의 스레드만 실행할 수 있다.
- 실행 결과를 보면 "t1"이 withdraw() 메서드를 시작부터 완료까지 모두 끝내고 나서, 그 다음에 "t2"가 withdraw() 메서드를 수행하는 것을 확인할 수 있다. 물론 환경에 따라 "t2"가 먼저 실행될 수도 있다. 이 경우에도 "t2"가 withdraw() 메서드를 모두 수행한 다음에 "t1"이 withdraw() 메서드를 수행한다.
Synchronized 분석

- 모든 객체(인스턴스)는 내부에 자신만의 락( lock )을 가지고 있다.
- 모니터 락(monitor lock)이라도고 부른다.
- 객체 내부에 있고 우리가 확인하기는 어렵다.
- 스레드가 synchronized 키워드가 있는 메서드에 진입하려면 반드시 해당 인스턴스의 락이 있어야 한다!
- 여기서는 BankAccount(x001) 인스턴스의 synchronized withdraw() 메서드를 호출하므로 이 인스턴스의 락이 필요하다.
- 스레드 t1 , t2 는 withdraw() 를 실행하기 직전이다.

- t1 이 먼저 실행된다고 가정하겠다.
- 스레드 t1 이 먼저 synchronized 키워드가 있는 withdraw() 메서드를 호출한다.
- synchronized 메서드를 호출하려면 먼저 해당 인스턴스의 락이 필요하다.
- 락이 있으므로 스레드 t1 은 BankAccount(x001) 인스턴스에 있는 락을 획득한다.

- 스레드 t1 은 해당 인스턴스의 락을 획득했기 때문에 withdraw() 메서드에 진입할 수 있다.
- 스레드 t2 도 withdraw() 메서드 호출을 시도한다. synchronized 메서드를 호출하려면 먼저 해당 인스턴 스의 락이 필요하다.
- 스레드 t2는 BankAccount(x001) 인스턴스에 있는 락 획득을 시도한다. 하지만 락이 없다. 이렇게 락이 없으면 t2 스레드는 락을 획득할 때까지 "BLOCKED" 상태로 대기한다.
- t2 스레드의 상태는 RUNNABLE BLOCKED 상태로 변하고, 락을 획득할 때까지 무한정 대기한다.
- 참고로 BLOCKED 상태가 되면 락을 다시 획득하기 전까지는 계속 대기하고, CPU 실행 스케줄링에 들어가지 않는다.

- t1 : 출금을 위한 검증 로직을 수행한다. 조건을 만족하므로 검증 로직을 통과한다.
- 잔액[1000]이 출금액[800] 보다 많으므로 통과한다.

- t1 : 잔액 1000원에서 800원을 출금하고 계산 결과인 200원을 잔액( balance )에 반영한다.

- t1 : 메서드 호출이 끝나면 락을 반납한다.

- t2 : 인스턴스에 락이 반납되면 락 획득을 대기하는 스레드는 자동으로 락을 획득한다.
- 이때 락을 획득한 스레드는 "BLOCKED" -> "RUNNABLE" 상태가 되고, 다시 코드를 실행한다.

- 스레드 t2는 해당 인스턴스의 락을 획득했기 때문에 withdraw() 메서드에 진입할 수 있다.
- t2 : 출금을 위한 검증 로직을 수행한다. 조건을 만족하지 않으므로 false를 반환한다.
- 이때 잔액( balance )은 200원이다. 800원을 출금해야 하므로 조건을 만족하지 않는다.

- t2: 락을 반납하면서 return 한다.
t1 은 800원 출금에 성공하지만, t2는 잔액 부족으로 출금에 실패한다. 그리고 최종 잔액은 1000원에서 200원이 되므로 정확하게 맞다.
이렇게 자바의 synchronized를 사용하면 한 번에 하나의 스레드만 실행하는 안전한 임계 영역 구간을 편리하게 만들 수 있다.
참고: 락을 획득하는 순서는 보장되지 않는다.
만약 BankAccount(x001) 인스턴스의 withdraw()를 수많은 스레드가 동시에 호출한다면, 1개의 스레드만 락을 획득하고 나머지는 모두 "BLOCKED" 상태가 된다. 그리고 이후에 BankAccount(x001) 인스턴스에 락을 반납하면, 해당 인스턴스의 락을 기다리는 수많은 스레드 중에 하나의 스레드만 락을 획득하고, 락을 획득한 스레드만 "BLOCKED" -> "RUNNABLE" 상태가 된다.
이때 어떤 순서로 락을 획득하는지는 자바 표준에 정의되어 있지 않다. 따라서 순서를 보장하지 않고, 환경에 따라서 순서가 달라질 수 있다.
참고: volatile를사용하지 않아도 synchronized 안에서 접근하는 변수의 메모리 가시성 문제는 해결된다.
Synchronized 코드 블럭
synchronized의 가장 큰 장점이자 단점은 한 번에 하나의 스레드만 실행할 수 있다는 점이다. 여러 스레드가 동시에 실행하지 못하기 때문에, 전체로 보면 성능이 떨어질 수 있다. 따라서 synchronized를 통해 여러 스레드를 동시에 실행할 수 없는 코드 구간은 꼭! 필요한 곳으로 한정해서 설정해야 한다.
그런데 메서드 앞에 적용한 synchronized의 적용 범위는 메서드 전체이다. 따라서 여러 스레드가 함께 실행해도 문 제가 없는 "거래 시작", "거래 종료"를 출력하는 코드도 한 번에 하나의 스레드만 실행할 수 있다. 자바는 이런 문제를 해결하기 위해 synchronized를 메서드 단위가 아니라, 특정 코드 블럭에 최적화해서적용할 수 있는 기능을 제공한다.



- withdraw() 메서드 앞에 사용하던 synchronized를 제거한다.
- synchronized (this) {} : 안전한 임계 영역을 코드 블럭으로 지정한다.
- 이렇게 하면 꼭 필요한 코드만 안전한 임계 영역으로 만들 수 있다.
- synchronized (this) :여기서 괄호() 안에 들어가는 값은 락을 획득한 인스턴스의 참조이다.
- 여기서는 BankAccountV3(x001)의 인스턴스의 락을 사용하므로 이 인스턴스의 참조인 this를 넣어 주면 된다.
- 이전에 메서드에 synchronized를 사용할 때와 같은 인스턴스에서 락을 획득한다.
- getBalance()의 경우 return balance 코드 한 줄이므로synchronized를 메서드에 설정하나 코드 블럭으로 설정하나 둘다 같다.
synchronized 블럭 기능을 사용한 덕분에 딱 필요한 부분에 임계 영역을 지정할 수 있었다. 덕분에 아주 약간이지만 여러 스레드가 동시에 수행되는 부분을 더 늘려서, 전체적으로 성능을 더 향상할 수 있었다. 지금의 예는 단순히 로그 몇 줄 출력하는 정도이지만, 만약 작업이 오래 수행된다면 큰 성능 차이가 발생할 것이다. 여기서 이야기하고 싶은 핵심은 하나의 스레드만 실행할 수 있는 안전한 임계 영역은 가능한 최소한의 범위에 적용해야 한다는 점이다. 그래야 동시에 여러 스레드가 실행할 수 있는 부분을 늘려서, 전체적인 처리 성능을 더 높일 수 있다.
synchronized 동기화 정리
자바에서 동기화(synchronization)는 여러 스레드가 동시에 접근할 수 있는 자원(예: 객체, 메서드)에 대해 일관성 있고 안전한 접근을 보장하기 위한 메커니즘이다. 동기화는 주로 멀티스레드 환경에서 발생할 수 있는 문제, 예를 들어 데 이터 손상이나 예기치 않은 결과를 방지하기 위해 사용된다.
메서드 동기화: 메서드를 synchronized로 선언해서, 메서드에 접근하는 스레드가 하나뿐이도록 보장한다.
public synchronized void synchronizedMethod() {
// 코드
}
블록 동기화: 코드 블록을 synchronized로, 동기화를 구현할 수 있다.
public void method() {
synchronized(this) {
// 동기화된 코드 }
}
이런 동기화를 사용하면 다음 문제들을 해결할 수 있다.
- 경합 조건(Race condition): 두 개 이상의 스레드가 경쟁적으로 동일한 자원을 수정할 때 발생하는 문제.
- 데이터 일관성: 여러 스레드가 동시에 읽고 쓰는 데이터의 일관성을 유지.
동기화는 멀티스레드 환경에서 필수적인 기능이지만, 과도하게 사용할 경우 성능 저하를 초래할 수 있으므로 꼭 필요한 곳에 적절히 사용해야 한다.
'멀티스레드와 동시성' 카테고리의 다른 글
| Ch06. 동기화 - 문제와 풀이 (0) | 2024.08.12 |
|---|---|
| Ch06. 동기화 - 출금 (0) | 2024.08.11 |
| Ch06. 동기화 - 임계 영역 (0) | 2024.08.10 |
| Ch06. 동기화 - 동시성 문제 (0) | 2024.08.10 |
| Ch05. 메모리 가시성 - 자바 메모리 모델 (0) | 2024.08.08 |