LockSupport1
` synchronized` 는 자바 1.0부터 제공되는 매우 편리한 기능이지만, 다음과 같은 한계가 있다.
synchronized의 단점
- 무한 대기 : BLOCKED 상태의 스레드는 락을 풀릴 때까지 무한 대기한다.
- 특정 시간까지만 대기하는 타임 아웃 X
- 중간에 인터럽트 X
- 공정성 : 락이 돌아왔을 때 BLOCKED 상태의 여러 스레드 중에 어떤 스레드가 락을 획득할지 알 수 없다. 최악의 경우 특정 스레드가 너무 오랜 기간 락을 획득하지 못할 수 있다.
자바 1.5부터 ` java.util.concurrent` 라는 동시성 문제 해결을 위한 라이브러리 패키지가 추가된다.
이 라이브러리에는 수 많은 클래스가 있지만, 가장 기본이 되는 ` LockSupport` 에 대해서 먼저 알아보자.
` LockSupport` 를 사용하면 ` synchronized` 의 가장 큰 단점인 무한 대기 문제를 해결할 수 있다.
LockSupport 기능
LockSupport 는 스레드를 WAITING 상태로 변경한다.
WAITING 상태는 누가 깨워주기 전까지는 계속 대기한다. 그리고 CPU 실행 스케줄링에 들어가지 않는다.
LockSupport의 대표적인 기능
- park(): 스레드를 WAITING 상태로 변경한다
- 스레드를 대기 상태로 둔다. 참고로 park의 뜻이 "주차하다" , "두다"라는 뜻이다.
- parkNanos(nanos)` : 스레드를 나노초 동안만 ` TIMED_WAITING` 상태로 변경한다.
- 지정한 나노초가 지나면 ` TIMED_WAITING` 상태에서 빠져나오고 ` RUNNABLE` 상태로 변경된다
- unpark(thread) : WAITTING 상태의 대상 스레드를 RUNNABLE 상태로 변경한다.
LockSupport 코드
package thread.sync.lock;
import java.util.concurrent.locks.LockSupport;
import static util.MyLogger.log;
import static util.ThreadUtils.sleep;
public class LockSupportMainV1 {
public static void main(String[] args) {
Thread thread1 = new Thread(new ParkTest(), "Thread-1");
thread1.start();
// 잠시 대기하여 Thread-1 이 park 상태에 빠질 시간을 준다.
sleep(100);
log("Thread-1 state : " + thread1.getState());
log("main -> unpark(Thread-1)");
LockSupport.unpark(thread1); // 1. unpark 사용
// thread1.interrupt(); // 2. interrupt() 사용
}
static class ParkTest implements Runnable {
@Override
public void run() {
log("park 시작");
LockSupport.park();
log("park 종료, state : " + Thread.currentThread().getState() );
log("인터럽트 상태: " + Thread.currentThread().isInterrupted());
}
}
}
15:08:11.182 [ Thread-1] park 시작
15:08:11.259 [ main] Thread-1 state : WAITING
15:08:11.260 [ main] main -> unpark(Thread-1)
15:08:11.261 [ Thread-1] park 종료, state : RUNNABLE
15:08:11.271 [ Thread-1] 인터럽트 상태: false
Process finished with exit code 0
- main ` 스레드가 Thread-1 을 start() 하면 Thread-1 은 RUNNABLE 상태가 된다.
- Thread-1 은 Thread.park() 를 호출한다. Thread-1 은 RUNNABLE -> WAITING 상태가 되면서 대기한다.
- main 스레드가 Thread-1 을 unpark()로 꺠운다. Thread-1 은 대기 상태에서 실행가능 상태로 변한다.
- WAITING -> RUNNABLE
인터럽트 사용
WAITING 상태의 스레드에 인터럽트가 발생하면 WAITNG 상태에서 RUNNABLE 상태로 변하면서 깨어난다.
위 코드에 주석을 다음과 같이 변경해보자
unpark 대신에 인터럽트를 사용해서 스레드를 깨워보자
15:37:09.120 [ Thread-1] park 시작
15:37:09.193 [ main] Thread-1 state : WAITING
15:37:09.194 [ Thread-1] park 종료, state : RUNNABLE
15:37:09.204 [ Thread-1] 인터럽트 상태: true
Process finished with exit code 0
실행 결과를 보면 스레드가 RUNNABLE 상태로 깨어난 것을 확인할 수 있다. 그리고 해당 스레드의 인터럽트의 상태도 true 인 것을 확인할 수 있다.
이처럼 WAITING 상태의 스레드는 인터럽트를 걸어서 중간에 깨울 수 있다.
LockSupport2
시간 대기
- 이번에는 스레드를 특정 시간 동안만 대기하는 parkNanos(nanos)를 호출해 보자.
- parkNanos(nanos) : 스레드를 나노초 동안만 ` TIMED_WAITING` 상태로 변경한다. 지정한 나노초가 지나면 TIMED_WAITING 상태에서 빠져나와서 ` RUNNABLE` 상태로 변경된다.
- 참고로 밀리초 동안만 대기하는 메서드는 없다. ` parkUntil(밀리초)`라는 메서드가 있는데, 이 메서드는 특정 에포크(Epoch) 시간에 맞추어 깨어나는 메서드이다. 정확한 미래의 에포크 시점을 지정해야 한다.
public class LockSupportMainV2_2 {
public static void main(String[] args) {
Thread thread1 = new Thread(new ParkTest(), "Thread-1");
thread1.start();
// 잠시 대기하여 Thread-1 이 park 상태에 빠질 시간을 준다.
sleep(100);
log("Thread-1 state : " + thread1.getState());
}
static class ParkTest implements Runnable {
@Override
public void run() {
log("park 시작, 2초 대기");
LockSupport.parkNanos(2000_000000); // parkNanos 사용
log("park 종료, state : " + Thread.currentThread().getState() );
log("인터럽트 상태: " + Thread.currentThread().isInterrupted());
}
}
}
- 여기서는 스레드를 깨우기 위한 ` unpark()` 를 사용하지 않는다. `
- parkNanos(시간)` 를 사용하면 지정한 시간 이후에 스레드가 깨어난다
15:46:20.331 [ Thread-1] park 시작, 2초 대기
15:46:20.409 [ main] Thread-1 state : TIMED_WAITING
15:46:22.344 [ Thread-1] park 종료, state : RUNNABLE
15:46:22.357 [ Thread-1] 인터럽트 상태: false
Process finished with exit code 0
- Thread-1 은 parkNanos(2초)를 사용해서 2초간 TIMED_WAITING 상태에 빠진다.
- Thread-1 은 2초 이후에 TIMED_WAITING를 빠져나온다.
BLOCKED vs WAITING
WA ITING 상태에 특정 시간까지만 대기하는 기능이 포함된 것이 ` TIMED_WAITING`이다. 여기서는 둘을 묶어서 ` WAITING` 상태라 표현하겠다.
인터럽트
BLOCKED 상태는 인터럽트가 걸려도 대기 상태를 빠져나오지 못한다. 여전히 BLOCKED 상태이다.
WAITING, TIMED_WAITING 상태는 인터럽트가 걸리면 대기상태를 빠져나온다. 그래서 RUNNABLE 상태로 변한다.
용도
- BLOCKED 상태는 자바의 synchronized에서 락을 획득하기 위해 대기할 때 사용된다.
- WAITING, TIMED_WAITING 상태는 스레드가 특정 조건이나 시간 동안 대기할 때 발생하는 상태이다.
- WAITING 상태는 다양한 상황에서 호출된다. 예를 들어, Thread.join(), LockSupport.park(), Object.wait()와 같은 메서드 호출 시 WAITING 상태가 된다.
- TIMED_WAITING` 상태는 ` Thread.sleep(ms),` ` Object.wait(long timeout)` , ` Thread.join(long millis)` , ` LockSupport.parkNanos(ns)` 등과 같은 시간제한이 있는 대기 메서드를 호출할 때 발생한다.
대기( ` WAITING` ) 상태와 시간 대기 상태( ` TIMED_WAITING` )는 서로 짝이 있다.
- ` Thread.join()` , ` Thread.join(long millis)`
- ` Thread.park()` , ` Thread.parkNanos(long millis)`
- ` Object.wait()` , ` Object.wait(long timeout)
BLOCKED , WAITING , TIMED_WAITING 상태 모두 스레드가 대기하며, 실행 스케줄링에 들어가지 않기 때문에, CPU 입장에서 보면 실행하지 않는 비슷한 상태이다.
- BLOCKED 상태는 synchronized에서만 사용하는 특별한 대기 상태라고 이해하면 된다.
- WAITING, TIMED_WAITING 상태는 범용적으로 활용할 수 있는 대기상태라고 이해하면 된다.
LockSupport 정리
` LockSupport` 를 사용하면 스레드를 ` WAITING` , ` TIMED_WAITING` 상태로 변경할 수 있고, 또 인터럽트를 받아서 스레드를 깨울 수도 있다. 이런 기능들을 잘 활용하면 ` synchronized` 의 단점인 무한 대기 문제를 해결할 수 있을 것 같다.
synchronized 단점
무한 대기 : BLOCKED 상태의 스레드는 락이 풀릴 때까지 무한 대기한다.
- 특정 시간까지만 대기하는 타임아웃 X -> parkNanos()를 사용하면 특정 시간까지만 대기할 수 있음
- 중간에 인터럽트 X -> ` park() ` , parkNanos()는 인터럽트를 걸 수 있음.
이처럼 LockSupport를 활용하며 , 무한 대기하지 않는 락 기능을 만들 수 있다.
물론 그냥 되는 것은 아니고 ` LockSupport `를 활용해서 안전한 임계 영역을 만드는 어떤 기능을 개발해야 한다.
예를 들면 다음과 같을 것이다.
if (!lock.tryLock(10초)) { // 내부에서 parkNanos() 사용
log("[진입 실패] 너무 오래 대기했습니다.");
return false;
}
//임계 영역 시작
...
//임계 영역 종료
lock.unlock() // 내부에서 unpark() 사용
락( ` lock ` park() )이라는 클래스를 만들고 , 특정 스레드가 먼저 락을 얻으면 RUNNABLE로 실행하고, 락을 얻지 못하면 park ()를 사용해서 대기 상태로 만드는 것이다. 그리도 스레드가 임계 영역의 실행을 마치고 나면 락을 반납하고, unpark()를 사용해서 대기 중인 다른 스레드를 깨우는 것이다. 물론 parkNanos() 를 사용해서 너무 오래 대기하면 스레드가 스스로 중간에 깨어나게 할 수도 있다.
하지만 이런 기능을 직접 구현하기는 매우 어렵다. 예를 들어 스레드 10개를 동시에 실행했는데, 그중에 딱 1개의 스레드만 락을 가질 수 있도록 락 기능을 만들어야 한다. 그리고 나머지 9개의 스레드가 대기해야 하는데, 어떤 스레드가 대기하고 있는지 알 수 있는 자료구조가 필요하다. 그래야 이후에 대기 중인 스레드를 찾아서 깨울 수 있다. 여기서 끝이 아니다. 대기 중인 스레드 중에 어떤 스레드를 깨울지에 대한 우선순위 결정도 필요하다.
한마디로 ` LockSupport ` 는 너무 저수준이다. 하지만 걱정하지 말자! 자바는 두었다. ReentrantLock ` ` synchronized ` ` Lock ` 인터페이스와 `처럼 더 고수준의 기능이 필요하다. ReentrantLock 은 ` LockSupport이라는 구현체로 이런 기능들을 이미 다 구현해 ` 를 활용해서 임계 영역을 다룰 수 있는 다양한 기능을 제공한다
참고 자료 : 김영한의 실전 자바 - 고급 1편, 멀티스레드와 동시성
'JAVA' 카테고리의 다른 글
ReentrantLock (0) | 2025.01.07 |
---|---|
Java에서의 지연 초기화,지연 평가 Optional 활용: orElse vs orElseGet (0) | 2025.01.06 |
생산자 소비자 문제1 (1) | 2024.12.31 |
메모리 가시성 (0) | 2024.12.29 |
익명 클래스 (0) | 2024.12.24 |