synchroinzed 와 DeadLock
synchroinzed
멀티스레드 환경에서 동기화를 제공하기 위해 사용
특정한 블록이나 메서드를 여러 스레드가 동신에 접근하지 못하도록 제어한다.
메서드 선언시 사용하면 해당 메서드는 해당 객체의 락을 획득하고, 메서드 실행이 완료될 때 락을 해제한다.
다른 스레드가 동일 객체에 접근하려면 락이 해제될 때까지 대기해야 한다.
특정 코드 블록에 사용할 수 있다.
DeadLock
멀티 스레드 환경에서 여러 개의 스레드가 서로 자원을 기다리면서 발생하는 무한 대기 상태
데드락 발생 조건
1. 상호 배제
자원은 한 번에 하나의 스레드만 사용할 수 있어야 한다.
만약 자원을 여러 스레드가 동시에 사용할 수 있다면, 자원을 얻기 위해 대기할 필요가 없어 데드락이 발생하지 않는다.
2. 점유 및 대기
최소한 하나의 자원을 점유하고 있으면서 다른 자원을 추가로 얻기 위해 대기하는 상태가 있어야 한다.
자원을 요청할 때 다른 자원을 보유하고 있지 않은 경우, 데드락이 발생하지 않는다.
3. 비선점
이미 할당된 자원을 강제로 빼앗을 수 없습니다.
즉, 자원을 점유한 스레드는 그 자원을 자발적으로 해제하기 전까지 해당 자원을 계속 사용한다.
자원을 강제로 해제할 수 있는 경우, 데드락이 발생하지 않는다.
4. 순환대기
두 개 이상의 스레드가 순환적으로 자원을 대기하고 있어야 한다.
예를 들어, 스레드 A가 자원 X를 가지고 있고, 스레드 B는 자원 Y를 가지고 있으며 자원 X를 기다리는 경우이다.
이 경우 순환 대기가 해결되지 않으면 데드락이 발생한다.
이 네 가지 조건을 모두 충족하는 경우 데드락이 발생한다.
synchronized 키워드를 사용하여 데드락 예방
synchronized 를 사용하면 코드 블록이나 메서드를 한 번에 하나의 스레드만 실행할 수 있도록 한다.
스레드 간의 동기화를 쉽게 할 수 있지만, 잘못 사용하는 경우 데드락이 발생할 수 있다.
1. 자원 획득 순서 정하기
여러 스레드가 여러 자원을 필요로 할 때, 자원을 획득하는 순서를 모든 스레드가 동일하게 정하면 데드락을 방지할 수 있다.
예를 들어, 스레드들이 자원 A와 B를 사용해야 한다면, 항상 A를 먼저 요청하고, 그 후에 B를 요청하도록 순서를 정한다.
1
2
3
4
5
synchronized(resourceA) {
synchronized(resourceB) {
// 필요한 로직
}
}
2. 중접퇸 synchronized 블록 피하기
중첩된 synchronized 블록은 데드락이 발생할 가능성이 높다.
가능한 경우, 중첩된 synchronized 블록을 피하거나, 중첩된 블록을 단일 블록으로 합쳐서 처리하는 것이 좋다.
중첩된 synchronized 블록을 사용할 수 있는 경우
- 자원 잠금 순서가 통일된 경우 여러 스레드가 동일한 자원들을 잠글 때, 항상 같은 순서로 잠금이 이루어지면 데드락이 발생할 가능성이 줄어든다.
- 자원이 한 방향으로만 잠금되는 경우
자원이 한 번 잠금된 이후 다시 잠금되지 않도록 코드를 설계함면 데드락이 발생할 가능성이 줄어든다.- 타임 아웃 설정 타임 아웃을 설정하면, 설정한 시간이 지나면 다른 작업을 시도하거나 오류를 처리할 수 있어 데드락을 예방할 수 있다.
3. 최소한의 범위에 사용
synchronized 블록의 범위를 최소화하여, 가능한 한 자원을 빨리 해제하는 것이 교착 상태의 가능성을 줄이고, 성능을 향상시킨다.
synchronized 블록의 범위를 작게 설정하려면
synchronized 블록 내에 꼭 필요한 코드만 포함시켜야 한다.
불필요한 작업이나 오래 걸리는 작업이 포함되는 경우,
다른 스레드가 불필요하게 대기하기 되어 데드락이 발생할 수 있다.
자원을 사용하는 경우만 synchronized를 사용해 보호하고, 이후 복잡한 계산이나 작업은 synchronized 블록 밖에서 처리하는 것이 좋다.
그리고 자원 접근을 한 곳에서 일관되게 관리하는 것이 데드락을 예방할 수 있다.
여러 곳에서 자원을 관리하면, 자원의 접근 순서가 섞이기 때문에 데드락이 발생할 수 있다.
ReentrantLock과 synchronized의 차이
synchronized
synchronized 는 자바 키워드로 간단하게 동기화 블록과 메서드를 구현할 수 있다.
자동으로 재진입이 가능하기 때문에 같은 스레드가 동일한 락을 여러 번 획득할 수 있다.
예외 발생 시 락이 자동으로 해제된다.
ReentrantLock
java.util.concurrent.locks 패키지의 클래스로 타임아웃 설정, tryLock, condition 등을 이용하여, 세밀한 제어가 가능하다.
명시적으로 락을 획득하고 해제하는 과정이 필요하며, 예외 발생시에 unlock()를 반드시 호출해서 락을 해제해야 한다.
타임 아웃을 설정하여 지정된 시간동안 락을 시도하며 성공시 true, 실패시 false를 반환한다.
1
2
3
4
5
6
7
8
9
10
if (lock.tryLock(10, TimeUnit.SECONDS)) { // 락 획득시 true 반환
try {
// 자원 사용
} finally {
lock.unlock();
}
} else { // 락 획득 실패시 false 반환
// 타임아웃 처리
}
ReentrantLock에서 unlock()을 호출하지 않는다면
락이 영구적으로 유지된다.
락을 해제하지 않으면 다른 스레드가 이 락을 획득할 수 없다.
따라서 해당 자원에 대한 접근이 불가능하게 되어 프로그램이 멈추거나 성능이 저하될 수 있다.
이로 인해 데드락이 발생할 수 있다.
자원 누수
락이 해제되지 않으면 해당 자원은 계속해서 점유된 상태로 남는다.
이로 인해 자원 누수가 발생할 수 있다.
락 재진입 제한
ReentrantLock은 재진입이 가능한 락이기 때문에 같은 스레드가 여러 번 락을 획들할 수 있다. 하지만 unlock를 호출하지 않으면 재진입 카운터가 감소하지 않아 다른 작업에서 이 락을 사용할 수 없다.
ReentrantLock의 tryLock() 메서드
tryLock()은 인수없이 호출 된다.
락을 즉시 시도하며, 락을 획득하면 true, 그렇지 않으면 false를 반환한다.
tryLock(long timeout, TimeUnit unit) 의 경우 지정된 시간 동안 락을 시도한다.
특정 시간 동안만 락을 시도하기 때문에 데드락 발생 가능성을 줄일 수 있다.
락을 오래 기다리지 않기 때문에, 락을 시도한 후 다른 작업을 수행할 수 있다.
락 경쟁이 치열한 상황에서 trylock()를 사용해 락을 시도한 후 다른 작업을 수행할 수 있어 유용하다.
또한 주기적으로 특정 자원에 대한 접근이 필요한 경우 유용하다.
tryLock()을 사용하면 스레드가 오래 기다리지 않고, 락 획득 실패 시 다른 작업을 수행할 수 있어 효율적인 자원 관리와 데드락 예방에 도움이 된다.
tryLock()이 실패하는 경우 다른 작업을 수행하도록 설계할 수 있으며, 일정 시간 후 다시 호출하는 방식으로 재시도 로직을 구현할 수도 있다.
스핀 락을 이용해 반복해서 락을 시도할 수 있다.
스레드가 락을 계속해서 시도하는 경우, CPU 리소스를 낭비할 수 있다.
이로 인해 시스템의 성능이 저하될 수 있다.
여러 스레드가 서로 다른 우선 순위로 락을 재시도하는 경우, 우선 순위가 낮은 스레드가 락을 먼저 획들하고, 우선순위가 높은 스레드가 락을 획득하지 못해 대기하는 상황이 발생할 수 있다. 이로 인해 우선 순위가 역전되거나, 더 나아가 교착상태가 발생할 수 있으며, 락 경쟁이 심화되어 시스템의 성능이 저하될 수 있다.
또한 특정 스레드가 자원을 거의 사용하지 못하는 경우가 발생할 수 있는데 이는 기아 상태로 빠질 수 있다.
tryLock(long timeout, TimeUnit unit)을 효율적으로 사용하려면
tryLock(long timeout, TimeUnit unit)을 사용할 때 효율적으로 활용하기 위해서는,
우선 해당 락을 얼마 동안 기다릴지 설정하는 것이 중요하다.
이 값을 너무 짧게 설정하면 락을 얻지 못하는 경우가 빈번해질 수 있고, 너무 길게 설정하면 대기 시간이 길어진다.
timeout의 값은 시스템 환경과 동작하는 스레드의 특성에 따라 조정해야 한다. 예를 들어, 빠르게 응답해야 하는 상황이라면 짧은 timeout 값을 선택하는 것이 좋다.
또한 unit으로 락을 대기하는 간격을 적절하게 조절할 수 있다. 일반적으로 TimeUnit.MILLISECONDS나 TimeUnit.SECONDS를 주로 사용하며, 상황에 따라 TimeUnit.MINUTES 등을 선택할 수도 있다.