Java

synchroinzed와 DeadLock

변위니 2025. 1. 7. 02:00

synchroinzed

멀티스레드 환경에서 동기화를 제공하기 위해 사용

여러 스레드가 동시에 동일한 자원에 접근하지 못하도록 제어한다. 

 

메서드 선언 앞에 사용되면 해당 메서드가 실행되는 동안 해당 객체의 락을 획득한다.
다른 스레드가 동일 객체에 접근하려면 락이 해제될 때까지 대기해야 한다.

메서드가 실행을 마치면 락을 해제하고, 다른 스레드가 접근할 수 있게 된다.

 

멀티 스레드 환경에서 공유 자원에 대한 경쟁 조건을 방지하기 위해서는 동기화가 필요하다.  

 

 

동기화의 목적

여러 스레가 공유 자원에 접근할 경우 발생할 수 있는 경쟁 조건을 방지하고, 자원의 일관성을 유지하기 위해 사용된다.

Synchronized는 해당 객체에 대한 단일 락을 관리하고, 스레드가 하나씩 실행되는 것을 보장한다.

 

여러 스레드가 동일한 변수에 접근하여 수정할 때 그 값이 예상과 다르게 변경되는 문제를 피할 수 있다.

 

 


DeadLock

멀티 스레드 환경에서 두 개 이상의 스레드가 서로  가지고 있는 자원을 기다리며 무한히 기다리는 상태를 말한다.

스레드들이 서로 가진 자원을 가지고 잠금한 채 기다리면서 무한 대기 상태에 빠지면 데드락이 발생한다.

 

데드락 발생 조건

1. 상호 배제 Mutual Exclusion

자원은 한 번에 하나의 스레드만 접근할 수 있어야 한다.

만약 자원을 여러 스레드가 동시에 사용할 수 있다면, 자원을 얻기 위해 대기할 필요가 없어 데드락이 발생하지 않는다.

2. 점유 및 대기 Hold and Wait

최소한 하나의 자원을 점유하고 있으면서 다른 자원을 추가로 얻기 위해 대기하는 상태가 있어야 한다.

자원을 요청할 때 다른 자원을 보유하고 있지 않은 경우, 데드락이 발생하지 않는다.

3. 비선점 Non-Preemption

이미 할당된 자원을 강제로 빼앗을 수 없다.
즉, 자원을 점유한 스레드는 그 자원을 자발적으로 해제하기 전까지 해당 자원을 계속 사용한다.
자원을 강제로 해제할 수 있는 경우, 데드락이 발생하지 않는다.

4. 순환대기 Circular Wait

두 개 이상의 스레드가 순환적으로 자원을 대기하고 있어야 한다.
예를 들어, 스레드 A가 자원 X를 가지고 있고, 스레드 B는 자원 Y를 가지고 있으며 자원 X를 기다리는 경우이다.
이 경우 순환 대기가 해결되지 않으면 데드락이 발생한다.

 

 

위 네 가지 조건을 모두 충족하는 경우 데드락이 발생한다.



 

 

데드락 방지하려면

synchronized 를 사용하면 코드 블록이나 메서드를 한 번에 하나의 스레드만 실행할 수 있도록 한다.
스레드 간의 동기화를 쉽게 할 수 있지만, 잘못 사용하는 경우 데드락이 발생할 수 있다.

1. 자원 획득 순서 정하기

여러 자원을 동시에 접근할 필요가 있을 때, 자원을 획득하는 순서를 모든 스레드가 동일하게 정하면 데드락을 방지할 수 있다.
예를 들어, 스레드들이 자원 A와 B를 사용해야 한다면, 항상 A를 먼저 요청하고, 그 후에 B를 요청하도록 순서를 정한다.

synchronized(resourceA) {
	synchronized(resourceB) {
    	// 필요한 로직
    }
}

 

2. 중접된 synchronized 블록 피하기

중첩된 synchronized 블록은 데드락이 발생할 가능성이 높다.
가능한 경우, 중첩된 synchronized 블록을 피하거나, 중첩된 블록을 단일 블록으로 합쳐서 처리하는 것이 좋다.

 

중첩된 synchronized 블록을 사용할 수 있는 경우

  • 자원 잠금 순서가 통일된 경우 
    여러 스레드가 동일한 자원들을 잠글 때, 항상 같은 순서로 잠금이 이루어지면 데드락이 발생할 가능성이 줄어든다.
  • 자원이 한 방향으로만 잠금되는 경우
    자원이 한 번 잠금된 이후 다시 잠금되지 않도록 코드를 설계함면 데드락이 발생할 가능성이 줄어든다.
  • 타임 아웃 설정 
    타임 아웃을 설정하면, 설정한 시간이 지난 후, 다른 작업을 시도하거나 오류를 처리할 수 있어 데드락을 예방할 수 있다.

 

3. 최소한의 범위에 사용

synchronized 블록의 범위를 최소화하여, 가능한 한 자원을 빨리 해제하자.

 

 

 

synchronized 블록의 범위를 작게 설정하려면

synchronized 블록 내에 꼭 필요한 코드만 포함시키자.
불필요한 작업, 오래 소요되는 작업이 포함되면, 다른 스레드가 불필요하게 대기하게 되어 데드락이 발생할 수 있다.

자원을 사용하는 경우만 synchronized를 사용하고, 이후 복잡한 작업은 synchronized 블록 밖에서 처리하자.

 

그리고 자원의 접근을 한 곳에서 일관되게 관리하자. 
여러 곳에서 자원을 관리하면, 자원의 접근 순서가 섞여 데드락이 발생할 수 있다.




 


synchronized과 ReentrantLock

synchronized

synchronized는 자바 키워드로 간단하게 동기화 블록과 메서드를 구현할 수 있다.

자동으로 재진입이 가능하기 때문에 같은 스레드가 동일한 락을 여러 번 획득할 수 있다.
예외 발생 시 락이 자동으로 해제된다.

ReentrantLock

java.util.concurrent.locks 패키지의 클래스로 타임아웃 설정, tryLock, condition 등을 이용하여, 세밀한 제어가 가능하다.
명시적으로 락을 획득하고 해제하는 과정이 필요하며, 예외 발생시에 unlock()를 반드시 호출해서 락을 해제해야 한다.
타임 아웃을 설정하여 지정된 시간동안 락을 시도하며 성공시 true, 실패시 false를 반환한다.

if (lock.tryLock(10, TimeUnit.SECONDS)) {   // 락 획득시 true 반환
    try {
        // 자원 사용
    } finally {
        lock.unlock();
    }
} else { // 락 획득 실패시 false 반환
    // 타임아웃 처리
}



ReentrantLock에서 unlock()을 호출하지 않으면

락이 영구적으로 유지된다.

락을 해제하지 않으면 다른 스레드가 이 락을 획득할 수 없다.
따라서 해당 자원에 대한 접근이 불가능하게 되어 프로그램이 멈추거나 성능이 저하될 수 있다.
이로 인해 데드락이 발생할 수 있다.

자원 누수

락이 해제되지 않으면 해당 자원은 계속해서 점유된 상태로 남는다.
이로 인해 자원 누수가 발생할 수 있다.

락 재진입 제한

ReentrantLock은 재진입이 가능한 락이기 때문에 같은 스레드가 여러 번 락을 획들할 수 있다.

하지만 unlock를 호출하지 않으면 재진입 카운터가 감소하지 않아 다른 작업에서 이 락을 사용할 수 없다.




ReentrantLock의 tryLock()

ReentrantLock의 tryLock() 메서드는 락을 시도하고 그 결과를 반환하는 메서드로, 두 가지 형태로 사용할 수 있다.

 

첫 번째로, 인수 없이 호출되는 tryLock()은 즉시 락을 시도하며, 락을 성공적으로 획득하면 true, 그렇지 않으면 false를 반환한다.

이 방식은 락 경쟁이 치열한 상황에서도 스레드가 오래 대기하지 않고 다른 작업을 수행할 수 있어 유용핟다. 특히, 주기적으로 특정 자원에 접근해야 하는 경우에 적합하다.

 

두 번째로, tryLock(long timeout, TimeUnit unit)은 지정된 시간 동안 락을 시도하는 메서드로, 설정된 시간 내에 락을 획득하지 못하면 실패를 반환한다. 이 방식은 데드락 발생 가능성을 줄이고, 락 대기 시간이 제한되기 때문에 시스템 성능을 유지하면서도 효율적으로 자원을 관리할 수 있다.

 

tryLock() 메서드를 활용하면 스레드가 락을 오래 기다리지 않고, 락 획득에 실패한 경우 다른 작업을 수행하거나 일정 시간 후 다시 락을 시도하는 재시도 로직을 구현할 수 있다. 이로 인해 효율적인 자원 관리와 데드락 예방이 가능하다.

 

그러나 tryLock()을 사용하는 경우에도 주의해야 할 부분이 있다.

스레드가 락을 반복해서 시도하는 경우, CPU 리소스를 낭비하여 시스템 성능이 저하될 수 있다. 특히, 여러 스레드가 서로 다른 우선순위로 락을 시도할 때, 우선순위가 낮은 스레드가 먼저 락을 획득하고 높은 우선순위의 스레드가 대기하는 우선순위 역전 문제가 발생할 수 있다.

 

더 나아가, 특정 스레드가 자원을 거의 사용하지 못하는 기아 상태로 이어질 수 있다. 

 

따라서, tryLock()은 적절한 상황에서 신중히 사용해야 하며, 스레드의 락 대기 전략과 시스템 성능 요구사항을 고려한 설계가 필요하다.

 

 

 

tryLock(long timeout, TimeUnit unit)을 효율적으로 사용하기 위해서는

적절한 timeout 값을 설정하는 것이 중요하다. timeout는 락을 얻기 위해 대기하는 시간으로 시스템의 환경과 스레드의 

특성에 따라 다르게 설정해야 한다.

너무 짧게 설정하면 락을 획득하기도 전 재시도 횟수가 많아져 성능 저하가 발생할 수 있다. 너무 길게 설정하면 락을 얻지 못한 스레드가 너무 오랫동안 대기하게 되어 시스템 전체의 응답성이 떨어질 수 있다. 중요한 작업을 해야 하는 스레드가 길게 대기하는 경우, 전체 시스템 성능에 악영향을 미칠 수 있다.  

 

timeout의 값은 시스템 환경과 동작하는 스레드의 특성에 따라 조정해야 한다. 예를 들어, 빠르게 응답해야 하는 상황이라면 짧은 timeout 값을 선택하는 것이 좋다.

 

TimeUnit은 시간을 측정하는 단위로 락을 대기하는 식나의 단위로 적절한 값을 선택하자.

일반적으로 TimeUnit.MILLISECONDS, TimeUnit.SECONDS를 주로 사용하며, 상황에 따라 TimeUnit.MINUTES 등을 선택할 수도 있다.

 



'Java' 카테고리의 다른 글

자바 직렬화 Serialization  (0) 2025.01.07
System.out.println()와 로그  (1) 2025.01.07
Generic과 Object  (0) 2025.01.07
Exception  (1) 2025.01.07
Process와 Thread  (2) 2025.01.07