본문 바로가기
인프런/김영한의 실전 자바 - 고급 1편, 멀티스레드와 동시성

[인프런] 김영한의 실전 자바 - 고급 1편, 멀티스레드와 동시성 / 7. 고급 동기화 - concurrent.Lock

by hxxyeoniii 2025. 3. 20.

LockSupport

synchronized는 자바 1.0부터 제공되는 편리한 기능이지만, 한계가 존재

 

(-) 무한 대기 : BLOCKED 상태 스레드는 락이 풀릴 때까지 무한 대기 -> 특정 시간까지만 대기하는 타임아웃 X, 중간에 인터럽트 X

(-) 공정성 : 락이 들어왔을 때 BLOCKED 상태의 여러 스레드 중 어떤 스레드가 락을 획득할지 알 수 없음 -> 최악의 경우 특정 스레드가 오랜기간 획득하지 못할 수도 있음

 

 

-> 더 유연하고 세밀한 제어를 위해 자바 1.5부터 java.util.concurrent라는 동시성 문제를 해결하기 위한 패키지 추가

-> 가장 기본이 되는 것은 LockSupport 클래스

 

 

 

LockSupport 기능

> 스레드를 WAITING 상태로 변경

> 누가 깨워주기 전까지 대기

 

1. park() : 스레드를 WAITING 상태로 변경

2. parkNanos(nanos) : 스레드를 나노초 동안 TIMED_WAITING 상태로 변경

3. unpark(thread) : WAITING 상태의 대상 스레드를 RUNNABLE 상태로 변경

 

 

 

LockSupportMainV1.java

package thread.sync.test.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(1000);
        log("Thread-1 state: " + thread1.getState());

        log("main -> unpark");
        LockSupport.unpark(thread1);
        log("Thread-1 state: " + thread1.getState());
    }

    static class ParkTest implements Runnable {

        @Override
        public void run() {
            log("park 시작");
            LockSupport.park();
            log("park 종료, state: " + Thread.currentThread().getState());
            log("인터럽트 상태: " + Thread.currentThread().isInterrupted());
        }
    }
}

 

실행 결과

 

실행 분석

 

 

 

parkNanos(nanos)

static class ParkTest implements Runnable {

        @Override
        public void run() {
            log("park 시작");
            LockSupport.parkNanos(2000_000000);
            log("park 종료, state: " + Thread.currentThread().getState());
            log("인터럽트 상태: " + Thread.currentThread().isInterrupted());
        }
    }

-> parkNanos(nanos) : 스레드를 나노초 동안만 TIMED_WAITING 상태로 변경

-> 2초 = 2,000,000,000 나노초 뒤에 스레드가 깨어남

 

 

 

BLOCKED vs WAITING

 

인터럽트 관점

> BLOCKED 상태는 인터럽트가 걸려도 대기 상태를 빠져나오지 못하고 여전히 BLOCKED 상태

> WAITING, TIMED_WAITING 상태는 인터럽트가 걸리면 RUNNABLE 상태로 변함

 

용도

> BLOCKED 상태는 자바의 synchronized에서 락을 획득하기 위해 대기할 때 사용

> WAITING 상태는 다양한 상황에서 사용 -> Thread.join(), LockSupport.park(), Object.wait() 등

 

=> BLOCKED, WAITING, TIMED_WAITING 모두 스레드가 대기하며 실행 스케줄링에 들어가지 않기 때문에 CPU 입장에서 보면 실행하지 않는 비슷한 상태

 

 

 

LockSupport 정리

synchronized의 무한 대기 문제를 해결할 수 있음

but LockSupport를 활용해 안전한 임계영역을 만드는 어떠한 기능을 개발해야지 가능..

예시 사진

-> 기능을 구현하기 힘듬, 고수준의 기능이 필요함

-> 자바는 Lock 인터페이스와 ReentrantLock 구현체로 이미 구현해두었다!


ReentrantLock - 이론

자바 1.5부터 Lock 인터페이스와 ReentrantLock 구현체를 제공함

 

Lock 인터페이스

-> 동시성 프로그래밍에서 쓰이는 안전한 임계 영역을 위한 락을 구현하는데 사용

 

 

 

void lock()

1. 락을 획득함, 만약 다른 스레드가 락을 획득했다면 락이 풀릴 때까지 WAITING 함

2. 인터럽트에 응답하지 않음!!

ex) 맛집에 한번 줄을 서면 끝까지 기다림, 친구가 중간에 다른 맛집을 찾았다고 해도 포기하지 않음

 

* 여기서 사용하는 락은 객체 내부의 모니터 락이 아님, Lock 인터페이스에서 제공하는 기능

* 모니터 락과 BLOCKED 상태는 synchronized에서만 사용

 

void lockInterruptibly()

1. 락 획득을 시도하되, 다른 스레드가 인터럽트할 수 있도록 함

2. 다른 스레드가 락을 획득했다면 현재 스레드는 락을 획득할 때까지 대기, 대기 중 인터럽트가 발생하면 InterruptedException이 발생하며 락 획득 포기

ex) 맛집에 줄을 서서 기다리다 친구가 다른 맛집을 찾았다고 연락하면 포기

 

boolean tryLock()

1. 락 획득을 시도하고, 즉시 성공 여부 반환

2. 다른 스레드가 락을 획득했다면 false, 그렇지 않으면 true 반환

ex) 맛집에 대기 줄이 없으면 바로 들어가고 대기 줄이 있으면 바로 포기

 

boolen tryLock(long time, TimeUnit unit)

1. 주어진 시간 동안 락 획득 시도, 시간 안에 락을 획득하면 true 반환

2. 대기 중 인터럽트가 발생하면 InterruptedException이 발생하며 락 획득 포기

ex) 맛집에 줄을 서지만 특정 시간 만큼만 기다림, 특정 시간이 지나도 줄을 서야 한다면 포기 & 친구가 다른 맛집을 찾았다고 하면 포기

 

void unlock()

1. 락 해제

2. 락을 해제하면 락 획득을 대기 중인 스레드 중 하나가 락을 획득

ex) 식당의 손님이 밥을 다 먹고 나가면 대기중인 손님 한명이 식당에 들어감

 

Contition newConditioin()

1. Condition 객체를 생성해 반환 .. 뒤에서 자세히

 

 

=> 위 함수들을 사용해 고수준 동기화 기법 구현 가능

=> syncronized 블록보다 더 많은 유연성을 제공하며 인터럽트가 가능한 락을 사용할 때 유용

 

 

참고 : lock()은 인터럽트에 응하지 않는다고 되어있으나 WAITING 상태 스레드는 인터럽트가 발생하면 대기 상태를 빠져나오지 않나?

lock()을 호출해 락을 얻기 위해 대기중인 스레드에 인터럽트가 발생하면 순간 대기 상태를 빠져나오지만, 다시 WAITING 상태로 강제 변경해버림 = 인터럽트 무시

 

 

 

공정성

synchronized의 공정성 문제는 아직 남아있다..

 

이를 위해 Lock 인터페이스의 구현체인 ReentrantLock이 있음

// 비공정 모드 락
private final Lock nonFairLock = new ReentrantLock(); 

//공정 모드 락
private final Lock fairLock = new ReentrantLock(true);

-> 공정성 모드와 비공정 모드로 설정할 수 있음

 

 

1. 비공정 모드(Non-fair mode)

: ReentrantLock의 기본 모드

: 락을 먼저 요청한 스레드가 락을 먼저 획득한다는 보장이 없음

 

(+) 성능 우선 : 락 획득 속도가 빠름

(+) 선점 가능 : 새 스레드가 기존 대기 스레드보다 먼저 락 획득 가능

(-) 기아 현상 가능성 : 특정 스레드가 계속 락을 획득하지 못할 수 있음

 

2. 공정 모드(Fair mode)

: 생성자에서 true 전달

: 락을 요청한 순서대로 스레드가 락 획득

 

(+) 공평성 보장 : 대기 큐에서 먼저 대기한 스레드가 락 획득

(+) 기아 현상 방지 : 모든 스레드가 언젠가 락을 획득하는 것이 보장

(-) 성능 저하 : 락 획득 속도가 느려질 수 있음


ReentrantLock - 활용

이전 출금 예제에 ReentrantLock 사용

...
private final Lock lock = new ReentrantLock();
...

@Override
public boolean withdraw(int amount) {
    log("거래 시작: " + getClass().getSimpleName());
    
    lock.lock(); // ReentrantLock 이용하여 lock을 걸기 
    
    try {
        log("[검증 시작] 출금액: " + amount + ", 잔액: " + balance); 
        if (balance < amount) {
            log("[검증 실패] 출금액: " + amount + ", 잔액: " + balance);
                  return false;
        }
    
        log("[검증 완료] 출금액: " + amount + ", 잔액: " + balance); 
        sleep(1000);
        balance = balance - amount;
        log("[출금 완료] 출금액: " + amount + ", 변경 잔액: " + balance);
        
    } finally {
        lock.unlock(); // ReentrantLock 이용하여 lock 해제
    }
    log("거래 종료"); return true;
}

-> lock ~ unlock 까지 안전한 임계 영역이 됨

-> 임계 영역을 수행하면 반드시 락을 반납해야 하므로 try-finally문을 사용해 finally 블럭에 unlock()을 호출해야 함!

-> 중간에 예외가 발생해도 unlock이 반드시 호출되도록

 

 

 

tryLock() 사용

if (!lock.tryLock()) {
    log("[진입 실패] 이미 처리중인 작업이 있습니다."); 
    return false;
}

-> 락을 획득할 수 없다면 false 반환하도록 작성

 

 

 

tryLock(시간)

try {
    if(!lock.tryLock(500, TimeUnit.MILLISECONDS)) {
        log("[진입 실패] 이미 처리중인 작업이 있습니다.");
        return false;
    }
} catch (InterruptedException e) {
   throw new RuntimeException(e);
}

-> 락이 없을 때 락을 대기할 시간 지정

-> 0.5초를 기다려도 락을 획득하지 못하면 false 반환

-> 스레드는 대기하는 동안 TIMED_WAITING이 되고, 대기 상태를 빠져나오면 RUNNABLE이 됨