Java/Thread

Thread (4) - 락킹 기법과 데드락

수수한개발자 2023. 11. 9.
728x90

DeadLock

 

교착상태라고 불리며 두 개 이상의 작업이 서로 상대방의 작업이 끝나기만을 기다리는 상태를 말합니다.

 

예제 코드로 살펴보겠습니다.

public void Thread1() {
	lock(A)
		lock(B)
			task
		unlock(B)
	unlock(A)
}


public void Thread2() {
	lock(B)
		lock(A)
			task
		unlock(A)
	unlock(B)
}

 

  1. 스레드 1이 lock(A)를 겁니다.
  2. 스레드 2이 lock(B)를 겁니다.
  3. 스레드 1이 B에 접근하려고 하자 락이 걸려있어 대기를 합니다.
  4. 스레드 2이 A에 접근하려고 하지만 락이 걸려있어 대기를 합니다.

위와 같이 스레드1과 스레드2가 서로 대기 상태에 빠지는것을 데드락, 교착상태라고합니다.

 

데드락이 일어나는 이유

상호배제

첫번째는 바로 상호 배제입니다.

한번의 한 스레드만 리소스에 접근할 수 있다는 뜻입니다.

하나의  객체 (A, B)에 하나의 스레드만 접근할 수 있습니다.

 

점유와 대기

두번째는 점유와 대기입니다.

최소 하나의 스레드가 리소스를 점유하며 다른 리소스에 대기합니다.

각 스레드가 A, B를 점유하고 다른 락을 기다리고 있습니다.

 

비선점할당

세번째는 비선점할당입니다.

스레드가 사용 완료할 때까지 리소스를 사용할수 없습니다.

다른 스레드의 리소스를 뻇을 수 없습니다.

해당 스레드의 리소스 사용이 끝날 때까지 기다려야합니다.

 

순환대기

네번째는 순환대기입니다.

이는 데드락  상태에 빠진 스레드에서 발견할 수 있는데 한스레드가 다른 스레드가 점유한 자원을 기다리고 또 다른 스레드는

이전 스레드가 점유한 자원을 기다리는 상황입니다.

 

이렇게 조건을 만족하면 데드락이 발생하게 되는데 데드락을 피해기 위해서는 위의 조건중 하나라도 충족하지 않게 만들어야 합니다.

 

 

 

ReentrantLock

ReentrantLock은 객체에 적용된 synchronized 키워드 처럼 작동한다.

하지만 동기화 블록이 아닌 명확한 락킹과 언락킹이 필요하다.

public void method() {
    ReentrantLock reentrantLock = new ReentrantLock();
    reentrantLock.lock();
    try {
        // do something
    } finally {
        reentrantLock.unlock();
    }
}

 

여기서 finally를 통해 unlock을 반드시 시켜주는것이 중요하다.

 

테스트 방법

  • getQueuedThreads :  사용하면 락을 기다리는 스레드목록을 반환합니다.
  • getOwner : 락을 가지고 있는 스레드를 반환합니다.
  • isHeldByCurrentThread :  현재 스레드에 락이 있으면 참을 반환합니다.
  • isLocked : 스레드에 락이 있는지 없는지 알려줍니다.

 

tryLock

tryLock 메서드를 사용하면 lock 메서드 처럼 lock 객체를 얻습니다.

객체가 있다면 lock 객체를 얻고 참을 반환합니다.

lock 객체가 없다면 스레드를 차단하고 메서드는 false를 반환 하고 다음 명령으로 넘어갑니다.

 

void defaultLock() {
	lockObject.lock();
	try {
		...
		task
		...
	} finally {
		lockObject.unlock();
	}
}

void tryLockExample() {
	if (lockObject.tryLock()) {
	lockObject.lock();
    try {
        ...
        task
        ...
    } finally {
        lockObject.unlock();
    } else {
    	....
    }
}
}

 

 

defaultLock 메서드는 스레드1에서 락을 잡고 있다면 스레드2에서는 락이 풀어지기만을 기다리지만

tryLockExample 메서드는 스레드1에서 락을 가지고 있다면 tryLock()이 true를 반환해 else 로직을 실행할 수 있게 됩니다.

 

 

 

 

세마포어

세마포어는 허가하고 권한을 부여합니다.

사용자 수를 특정 리소스나 리소스나 리소스 그룹을 제한하는 데 사용할 수 있다.

리소스당 사용자 한명만 허가하는 락과 달리 세마포어는 사용자 수가 많든 적든 사용자 수를 리소스에 제한할 수 있다.

 

세마포어는 허가할 수 있는 초기값을 세팅합니다.

acquire() 메서드를 호출하면 허가를 얻습니다.

acquire(NUMBER_OF_PERMITS); 숫자를 인자로 전달하면 한번에 허가를 여러개 얻을 수 있습니다.

 

release(5) 메서드는 숫자를 인자로 전달하여 허가를 내줄 수 있습니다.

허가를 얻는것은 리소스에 락을 거는 것을 의미한다.

릴리스할때까지 다른 스레드는 세마포어를 얻을 수 없습니다.

 

 

세마포어의 특징

소유자 스레드 개념이 없다 -> 스레드 여러 개가 허가를 얻기 때문.

스레드 하나가 세마포어를 여러번 얻을 수 있다. -> 그래서 초기값을 1로 맞춘 이진 세마포어는 블록된다.

 

 

어떤 스레드이든지 세마포어를 릴리스한다.

세마포어를 얻지 않는 스레드도 세마포어를 릴리스  할 수 있다.

 

 

세마포어를 사용해야하는 경우

생산자 - 소비자 시나리오에서 사용하면 효율적으로 사용할 수 있습니다.

 

Semaphore full = new Semaphore(0);
Semaphore empty = new Semaphore(1);
Item item = null;

 

Producer

while(true) {
	empty.acquire();
	item = producerNewItem();
	full.release();
}

 

Consumer

while(true) {
	full.acquire();
	consume(item);
	empty.release();
}

 

  1. 소비자가 full.acquire() 세마포어에서 허가를 기다립니다.
  2. 생산자는 empty.acquire() 세마포어를 사용해 허가를 받습니다. -> 기본적으로 1개를 허가해줬으므로 바로 허가를 받을 수 있습니다.
  3. 생산자에서 full.release()를 통해 세마포어의 허가를 내줍니다.
  4. 소비자에서 허가를 받았으니 item 공유 리소스에 접근합니다.
  5. empty().release()를 통해 허가해줍니다.

다수의 스레드에서 접근을 할 때에는 큐를 만들고 큐에 ReentrantLock을 걸고 Item을 큐에 넣고 사용하면 됩니다.

 

 

 

Java의 모든 클래스가 객체클래스에서 상속하기에 조건변수로 사용할 수 있다.

 

wait()

다른 스레드가 꺠어날때까지 현재 스레드를 기다리게 합니다.

이 상태에서는 스레드가 CPU를 전혀 사용하지 않는데 wait  상태의 스레드를 꺠우는데에는 두가지 방법이 있다.

하나는 다른 스레드에서 notify() 메서드를 호출하는 것이다.

또 다른 하나는 notifyAll() 이다.

 

notify()

notify()는 현제 객체에서 대기하는 단일 스레드를 깨워줍니다.

여러 스레드가 객체에서 대기하고 있다면 그중 하나가 임의로 선택됩니다.

 

notifyAll()

객체에 모든 스레드를 꺠우는 방법이다.

 

위의 메서드들의 사용할 때의 주의점은 객체를 동기화해야 한다는것입니다.

 

public class MySharedClass {
	private boolean isComplete = false;
    
    public void synchronized waitUntilComplete() {
    	while(!isComplete) {
        	wait();
        }
    }
    
    public void synchronized complete() {
    	isComplete = true;
        notify();
    }
}

 

synchronized 키워드를 통해 동기화 시켜준 뒤 스레드 1, 2 를 통해 각각 wait()과 notify()를 통해 대기하고 꺠워줄수 있습니다.

728x90

댓글