* 해당 포스팅은 김영한님의 고급 자바편 강의를 수강 후 해당 내용을 정리한 포스팅입니다.
안녕하세요 이제 가을이 다가오는데 아직까지도 날씨가 덥고 올해 추석은 최고 35도까지 올라간다고 하네요..
항상 건강하시고 즐거운 연휴 보내시길 바랍니다 ㅎㅎ
지난 포스팅에서는 동시성 이슈에 대해 알아보았습니다.
동시성 이슈가 뭔지, 예제를 통해서 동시성 이슈는 정말 엄청난 버그를 확인했죠
오늘은 이어서 그렇다면 자바진영에서는 이 동시성 문제를 어떻게 해결했는지 알아보시죠!
임계 영역
이런 문제가 발생한 근본 원인은 여러 스레드가 함께 상요하는 공유 자원을 여러 단계로 나누어 사용하기 때문입니다.
우리가 지난 시간에 간단한 출금예제를 통해 동시성 이슈를 살펴보았는데
해당 예제를 다시 가져오보도록 하겠습니다.
출금() {
1. 검증 단계: 잔액(balance) 확인
2. 출금 단계: 잔액(balance) 감소
}
위의 문제는 결국 공유 자원을 여러 단계로 나누어 사용하기 때문
해당 로직에는 큰 가정이 존재합니다.
스레드 하나의 관점에서 출금 ()을 보면 1. 검증단계에서 확인한 잔액 1000원은 2. 출금단계에서 계산을 끝마칠 때 까지 같은 1000원으로 유지되어야 합니다. 그래야 검증 단계에서 확인한 금액으로, 출금단계에서 정확한 잔액을 계산할 수 있습니다.
그래야 검증단계에서 확인한 1000원에 800원을 차감해서 200원이라는 잔액을 정확하게 계산 할 수 있습니다.
결국 내가 사용하는 값이 중간에 변경되지 않을 것 이라는 가정이 존재합니다
그런데 만약 중간에 다른 스레드가 잔액의 값을 변경한다면, 큰 혼란이 발생합니다. 1000원이라고 생각한 잔액이 다른 값으로 변경되면 잔액이 전혀 다른 값으로 계산될 수 있습니다.
공유자원인 balance는 여러 스레드가 함께 사용하는 공유 자원입니다. 따라서 출금 로직을 중간에 다른 스레드에서 이 값을 얼마든지 변경할 수 있습니다.
결국 해결 방법은 무엇일까요?
여러개의 스레드가 동시에 공유자원을 접근하는데에서 충돌이 일어난 문제가 동시성 문제입니다.
가장 간단하게 해결책은 한 번에 하나의 스레드만 실행 하는것입니다.
만약에 출금()이라는 메서드를 한 번에 하나의 스레드만 실행할 수 있게 제한한다면요?
위 그림 과 다르게 t1 스레드가 메모리에 저장한 잔액 변수를 변경하는 도중에 t2는 withdraw 함수를 실행하는 것이 아닌 밖에서 대기를 한다면요 동시성 이슈가 일어날 수 있을까요?
결론적으로 출금 메서드가 진행할 때 잔액을 검증하는 단계부터 잔액의 계산을 완료하는 시점까지 잔액의 값은 중간에 변하면 안됩니다.
검증과 계산 이 두 단계는 한 번에 하나의 스레드만 실행해야 합니다. 그래야 잔액이 중간에 변하지 않고, 안전하게 계산을 수행할 수 있습니다.
임계 영역 (critical section)
여러 스레드가 동시에 접근하면 데이터 불일치나 예상치 못한 동작이 발생할 수 있는 위험하고 또 중요한 코드 부분을 뜻합니다.
우리가 지금 살펴보고있는 출금 예제가 바로 임계영역입니다. 더 자세히는 출금을 진행할 때 잔액을 검증하는 단계부터 잔액의 계산을 완료할 때 까지 임계영역입니다. 즉 잔액(balance)는 여러 스레드가 동시에 접근하면 안되는 공유자원입니다.
synchronized 메서드
자바에선은 이러한 동시성 이슈를 해결하기 위해 synchronized 메서드를 준비했습니다
사용법은 간단합니다. 임계영역이 일어날 함수에 그저 synchronized 키워드만 넣어주면 해결됩니다.
아래 처럼요!
synchronized 출금() {
1. 검증 단계: 잔액(balance) 확인
2. 출금 단계: 잔액(balance) 감소
}
사용법은 간단하니깐 어떻게 이렇게 마법처럼 키워드 하나 추가로 동시성 이슈 문제가 해결되는지 자세히 들여다 봅시다.
synchronized 분석
자바의 모든 객체는 내부에 자신만의 lock (락)을 가지고 있습니다.
모니터 락이라고 부르기도 합니다.
스레드가 synchronized 키워드가 있는 메서드에 진입하려면 반드시 해당 인스턴스의 락이 있어야합니다.
여기서 BankAccount 인스턴스의 synchronized withdraw 메서드를 호출하므로 이 인스턴스의 락이 필요합니다.
t1, t2는 withdraw()를 실행하기 직전 상태
t1이 먼저 실행된다고 가정했을때
스레드 t1이 먼저 synchronized 키워드가 존재하는 메서드에 호출합니다
해당 메서드를 호출하려면 락을 먼저 획득해야합니다
t1 스레드가 락을 획득한 상태에서 t2가 해당 메서드에 진입하려고 하나
락을 객체 내부에 락이 없으므로 실행하지 못하고 blocked 상태에 진입합니다.
t2의 스레드 상태는 Runnable -> Blocked 상태로 변하고, 락을 획득할 때 까지 무한정 대기상태로 들어갑니다.
t1 스레드가 출금을 위해 검증 로직을 수행합니다. 조건을 만족하므로 검증 로직을 통과합니다.
t1 스레드에서 1000원에서 800원을 계산하고 계산 결과인 200원을 잔액에 반영합니다.
t1 스레드는 메서드 호출이 끝나고 락을 반납합니다.
t2 인스턴스에 락이 반납되면 락 획득을 대기하는 스레드는 자동으로 락을 획득합니다.
이때 락을 획득한 스레드는 Blocked -> Runnable 상태가 되고 다시 코드를 실행합니다.
t2 스레드는 락을 획득하고 검증단계를 수행하지만 잔액 부족으로 검증로직을 통과하지 못합니다.
t2는 락을 반납하고 return 합니다.
* 참고로 어떤 스레드가 먼저 락을 획득하는지 순서는 보장하지 않습니다.
자바의 synchronized 메서드는 가장 큰 장점이자 단점은 한번에 하나의 스레드에서만 실행할 수 있다는 점입니다.
여러 스레드가 동시에 실행하지 못하기 때문에, 전체로 보면 성능이 떨어질 가능성이 큽니다. 따라서 synchronized 키워드는
신중하게 꼭 필요한 구간에서만 사용해야합니다.
그래서 이점을 보안하기 위해 메서드에 붙이는 키워드 뿐만이 아니라 아래 처럼 synchronized 블럭을 사용하여
개발자가 직접 임계영역을 설정하고 해당 구간만 동기화를 구현할 수 있습니다.
public void method() {
synchronized(this) {
// 동기화된 코드
}
}
이처럼 여러 스레드가 동시에 접근할 수 있는 자원에 대해 일관성 있고 안전한 접근을 보장하기 위한 키워드 덕분에
경합조건 (Race condition), 데이터 일관성 문제를 해결했습니다.
오늘의 포스팅은 여기까지입니다.
자바에서 제공해주는 키워드를 통해 개발자가 간편하게 동기화를 구현할 수 있었고, 자바 객체 내부의 lock을 활용했다는 사실도 알게 되었습니다. 다음 시간에는 생성자, 소비자 문제에 대해 포스팅을 올리도록 하겠습니다
감사합니다!
'OS' 카테고리의 다른 글
동기화 - synchronized (동시성 이슈) (0) | 2024.09.10 |
---|---|
자바 volatile, 메모리 가시성 문제 해결 (0) | 2024.09.09 |
OS 스레드의 예외 처리 (0) | 2024.09.03 |
Java 스레드 Deep Dive (4) | 2024.09.02 |
[컨텍스트 스위칭] 스레드 2번째 이야기 (0) | 2024.08.31 |