Synchronized의 단점
BLOCKED
상태의 스레드는 락일 풀릴때 까지 무한 대기한다.BLOCKED
상태의 여러 스레드 중에 어떤 스레드가 락을 획득할지 알 수 없다. 최악의 경우 특정 스레드가 너무 오랜기간 락을 획득하지 못할 수 있다.자바에서는 이 같은 문제를 해결하기 위해 여러 라이브러리를 제공하는데, 그중에서 가장 기본인 LockSupport
를 사용해보자.
LockSupport 기능
LockSupport
는 스레드를 Waiting
상태로 변경한다.
Waiting
상태는 누가 깨워주기 전까지는 계속 대기한다. 그리고 CPU 실행 스케줄링에 들어가지 않는다.
park():
스레드를 Waiting
상태로 변경한다.parkNanos(nanos):
스레드를 나노초 동안만 Timed_Waiting
상태로 변경한다.unpark(thread):
Waiting
상태의 대상 스레드를 Runabble
상태로 변경한다.public class LockSupportMainV1 {
public static void main(String[] args) {
Thread thread1 = new Thread(new ParkTask(), "Thread-1");
thread1.start();
// 잠시 대기하여 Thread-1이 park 상태에 빠질 시간을 준다. sleep(100);
log("Thread-1 state: " + thread1.getState());
log("main -> unpark(Thread-1)"); LockSupport.unpark(thread1); //
}
static class ParkTask implements Runnable {
@Override
public void run() {
log("park 시작");
LockSupport.park();
log("park 종료, state: " + Thread.currentThread().getState());
log("인터럽트 상태: " + Thread.currentThread().isInterrupted());
}
}
}
Thread-1
을 start()
하면 Thread-1
은 Runnable
상태가 된다.Thread-1
은 Thread.park()
를 호출한다. Runnable->Waiting
상태가 되면서 대기한다.Thread-1
을 unpark()
로 깨운다. Thread-1
은 대기 상태에서 실행 가능 상태로 변한다.Waiting->Runnable
상태로 변한다.시간대기
parkNanos(nanos):
스레드를 나노초동안만 Timed_Waiting
상태로 변경한다. 지정한 나노초가 지나면 Timed_Waiting
상태에서 빠져나와 Runnable
상태로 변경된다.BLOCKED vs WAITING
인터럽트
Blocked
상태는 인터럽트가 발생해도 대기 상태를 빠져나오지는 못한다. 여전히 Blocked
상태이다.Waiting
,Tiemd_Waiting
상태는 인터럽트가 걸리면 대기상태를 빠져나와 Runnable
용도
Blocked
상태는 자바의 synchronized
에서 락을 획득하기 위해 대기할 때 사용된다.Waiting
,Tiemd_Waiting
상태는 스레드가 특정 조건이나 시간동안 대기할 때 발생하는 상태이다.Waiting
상태는 Thread.join()
,LockSupport.park()
,Object.wait()
같은 메서드 호출시 Waiting
상태가 된다.
Blocked
,Waiting
,Tiemd_Waiting
상태 모두 스레드가 대기하며 실행 스케줄링에 들어가지 않기 때문에 CPU 입장에서 보면 실행하지 않는 상태이다.
BLOCKED
상태는 synchronized
에서만 사용하는 특별한 대기 상태라고 이해하면 된다.WAITING
, TIMED_WAITING
상태는 범용적으로 활용할 수 있는 대기 상태라고 이해하면 된다.
package java.util.concurrent.locks;
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
Lock
인터페이스는 동시성 프로그래밍에서 쓰이는 안전한 임계 영역을 위한 락을 구현하는데 사용된다.
Lock
인터페이스는 아래와 같은 메서드를 제공하며 대표적인 구현체로는 ReebtrantLock
이 있다.
void lock()
void lockInterruptibly()
InterruptedException
이 발생하며 락 획득을 포기한다. boolean tryLock()
false
를 반환하고, 그렇지 않으면 락을 획득하고 true
를 반환한다. boolean tryLock(long time, TimeUnit unit)
true
를 반환한다. 주어진 시간이 지나도 락을 획득하지 못한 경우 false
를 반환한다. 이 메서드는 대기 중 인터럽트가 발생하면 InterruptedException
이 발생하며 락 획득을 포기한다. void unlock()
IllegalStateException
이 발생할 수 있다.Lock
인터페이스가 제공하는 다양한 기능 덕분에 synchronized
의 단점인 무한 대기 문제는 해결되었지만 공정성에 관한 문제가 남아있다.이런 공정성 문제는 ReentrantLock(true)
이 해결해준다.
사용예시
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockEx { // 비공정 모드 락
private final Lock nonFairLock = new ReentrantLock(); // 공정 모드 락
private final Lock fairLock = new ReentrantLock(true);
public void nonFairLockTest() {
nonFairLock.lock();
try {
// 임계 영역
} finally {
nonFairLock.unlock();
}
}
public void fairLockTest() {
fairLock.lock();
try {
// 임계 영역
} finally {
fairLock.unlock();
}
}
}
ReentrantLock
은 공정성 모드와 비공정 모드로 설정 할 수 있으며, 두 모드는 락을 획득하는 방식이 다르다.
비공정 모드 특징
공정 모드