[CS/운영체제] 멀티스레드와 동시성 - 8부

황제연·2025년 7월 1일
0

CS학습

목록 보기
122/193
post-thumbnail

공유자원을 접근할 때 발생하는 문제

동일한 인스턴스에서 상태가 변하는 변수가 있다고 가정한다고 했을 때,
동시에 두 스레드에서 해당 자원에 접근한다고 한다면 예상했던 것과 다른 문제를 마주할 수 있습니다

각각 다른 인스턴스의 Task에서 동일한 인스턴스의 상태가 변하는 값을 차감한다고 했을 때,
그 값이 0보다 작아지면 안되는 상태에서 0보다 작아질 수 있습니다
이 문제는 volatile로도 해결할 수 없습니다.
volatile은 단순히 메모리 가시성 문제를 해결하기 위함이지,
공유자원에 동시에 접근했을 때 발생하는 문제를 해결하기 위함은 아닙니다

동시성 문제

차감 중복

0보다 작아지지 않도록 하는 검증 코드가 앞에 있고, 이후 차감하는 작업이 진행된다고 하면
1번 스레드에서 먼저 검증 코드를 지난 뒤, 차감하는 작업을 진행할 때
2번 스레드가 검증 코드를 마주한다면, 아직 1번 스레드의 결과가 반영되지 않았기 때문에
검증 코드를 건너뛸 수 있습니다

이렇게 되면 중복으로 차감되는 문제가 발생할 수 있습니다

차감 작업 손실

또 다른 예시로 동시에 두 스레드가 검증 로직을 통과하고, 차감 코드를 실행한다고 하면,
동일한 초기값에서 동시에 차감되므로 두 스레드의 차감 결과가 동시에 반영되어서
한 스레드의 값이 전혀 반영되지 않는 스레드의 차감 작업이 손실되는 문제가 발생할 수 있습니다

임계영역

이런 문제가 발생한 근본적인 원인은 여러 스레드가 공유 자원을 여러 단계로 나누어서 사용하기 때문입니다
앞선 예시에서는 검증단계와 차감단계가 각각 순서대로 이루어지고 있습니다
이 로직은 검증단계에서 차감단계까지 공유자원이 초기값을 유지해야함을 보장해야합니다

하지만 중간의 다른 스레드가 공유자원의 값을 변경한다면 이 로직은 무결성이 깨집니다

한번에 하나의 스레드만 실행하자

따라서 이 작업자체를 한번에 하나의 스레드만 실행하도록 제한해야합니다
이렇게 한다면, 앞선 로직의 보장조건이 만족하게 되고, 안전하게 작업을 수행할 수 있습니다

임계영역(Critical Section)이란?

임계영역은 여러 스레드가 동시에 접근할 때 데이터 불일치나 의도치않은 동작이
발생할 수 있는 위험하고 중요한 코드영역을 말합니다

즉, 여러 스레드가 동시에 접근하면 안되는 공유자원을 접근하거나 수정하는 부분을 말합니다

앞선 차감로직이 바로 임계영역입니다
더 확장하면 검증단계까지 임계경역입니다

또한 공유자원은 여러 스레드가 동시에 접근해서는 안되며,
이런 임께영역은 한번에 하나의 스레드만 접근할 수 있도록 해야합니다

synchronized 메소드

바로 이런 임계영역에서 한번에 하나의 스레드만 접근할 수 있도록 안전하게 보호나는 방법은
synchronized를 사용하는 것입니다

사용방법은 매우 간단합니다
기존에는 이렇게 임계영역이되는 메소드가 있다고 가정하면

public void logic(){
	// 1. 검증
	// 2. 차감
}

이렇게 synchronized 키워드만 추가하면 됩니다

public synchronized void logic(){
	// 1. 검증
	// 2. 차감
} 

락 (lock)

이렇게 synchronized 키워드를 추가하면 처음 접근한 스레드는 락을 획득합니다
모든 인스턴스는 내부에 자신만의 락을 가지고 있습니다
그리고 이것을 모니터 락이라고 부르며, 객체 내부에 있기 때문에 직접 확인하기는 쉽지 않습니다

어떤 스레드가 synchornized 키워드가 있는 메소드에 진입하려면
해당 인스턴스에 락이 있어야합니다

앞선 예제에서 1번 스레드가 락을 획득하면, 2번 스레드는 락을 획득할 때까지
BLOCKED 상태로 대기합니다
그리고 락을 획득할 때까지 무한정 대기합니다

참고로 BLOCKED 상태가 되면 CPU 실행 스케줄링에 들어가지 않습니다

락의 획득 순서는?

락을 획득하는 순서는 보장하지 않습니다
1개의 스레드만 락을 획득하고 나머지는 모두 BLOCKED되며, 어떤 순서로 락을 획득하는지
자바 표준에는 정의되어 있지않습니다

참고로 synchronized 키워드를 사용하면 volatile을 사용하지 않아도 해당 키워드 내에서 접근하는
변수는 메모리 가시성 문제가 해결됩니다

synchronized의 문제점은?

synchronized를 사용해서 동시성 문제는 해결했지만 하나의 스레드만 실행할 수 있기 때문에
전체 성능이 저하될 수 있습니다

따라서 synchronized를 사용해서 여러 스레드를 동시에 실행할 수 없는 구간은
필요한 곳으로 한정해서 설정해야합니다

public synchronized void logic(){
	// 0. 로그 출력
	// 1. 검증
	// 2. 차감
	// 3. 공유자원을 사용하지 않는 작업
} 

위와 같은 코드 예시에서 0번과 3번은 synchronized 키워드가 필요없는 영역입니다
두 영역은 공유자원을 사용하지 않기 때문에, 여러 스레드가 동시에 접근해도 큰 문제가 되지 않습니다

1번과 2번 영역만이 synchronized가 필요한 영역입니다
따라서 자바에서는 메소드 단위만이 아닌 특정 코드 블록에 적용할 수 있도록 기능을 제공합니다

synchronized 코드 블록

public void logic(){
	// 0. 로그 출력
	synchronized (this){
		// 1. 검증
		// 2. 차감
	}	
	// 3. 공유자원을 사용하지 않는 작업
} 

이렇게 synchronized 코드 블록 기능을 사용해서 필요한 부분에만 임계영역을 지정할 수 있습니다
이로인해 여러 스레드가 동시에 수행되는 부분을 더 확장해서 전체적인 성능을 향상시킬 수 있습니다

synchronized 동기 화정리

자바의 동기화는 여러 스레드가 동시에 접근할 수 있는 자원에 대해 일관성 있고
안전한 접근을 보장하기 위한 기능입니다

동기화는 멀티스레드 환경에서 발생할 수 있는 데이터 손실이나 의도치 않은 결과를 막기 위해 사용합니다

메소드 동기화

메소드를 synchronized로 선언해서 메소드에 접근하는 스레드가 하나뿐이도록 보장합니다

public synchornized void syncMethod(){
	// logic
}

블록 동기화

코드 블록을 synchronized로 감싸서 동기화를 구현할 수 있습니다

public void method(){
	syncrhonized(this){
		// sync logic
	}
}

이러한 동기화를 사용해서 Race Condition 문제를 막고, 데이터 일관성을 유지할 수 있습니다

Race Condition

두 개 이상의 스레드가 동일한 자원을 경쟁해서 수정할 때 발생하는 문제입니다

데이터 일관성

여러 스레드가 동시에 읽고 쓰는 데이터의 일관성을 유지하는 것입니다

정리

동기화는 멀티스레드 환경에서 필수적이지만 과도하게 사용할 경우 성능 저하를 발생하기 떄문에
필요한 곳에서만 적절히 사용해야 합니다

지역변수와 final 키워드

지역변수의 경우 각 스레드의 개별 스택 프레임에 저장됩니다
즉, 공유되는 값이 아니므로 동시성 문제의 대상이 아닙니다

또한 생성자 주입으로 초기화되는 final 키워드도 최초 초기화 이후, 값이 변경되지 않습니다
그렇기 때문에 이것 역시 데이터의 일관성을 유지할 수 있기 때문에
동시성 문제의 대상이 아닙니다

syncrhonized 정리

장점

  • 자바 언어에서 문법으로 제공하며 사용이 편리함
  • 자동 잠금해제: 메소드나 블록이 완료되면 자동으로 락을 대기중인 다른 스레드의 잠금이 해제됩니다

단점

  • 무한대기: BLOCKED 상태의 스레드는 락이 풀릴 때까지 무한대기합니다
    특정시간까지만 대기하는 타임아웃이 존재 하지 않고, 중간에 인터럽트도 발생하지 않습니다
  • 공정성: 락이 돌아왔을 때, BLOCKED 상태의 여러 스레드 중에서 어떤 스레드가 락을 얻을 수 있는지 알 수 없습니다
    최악의 경우에는 특정 스레드가 너무 오랜기간 락을 획득하지 못할 수 있습니다

자바에서는 이러한 문제를 더 유연하고 세밀한 제어를 통해 해결하기 위해,
1.5부터는 java.util.concurrent라는 동시성 문제 해결을 위한 패키지가 추가되었습니다

결론

단순하고 편리하게 사용하거나 사용목적에 맞다면 synchronized를 사용하면 됩니다

참고

  • 김영한의 실전 자바 - 고급 1편
profile
Software Developer

0개의 댓글