JAVA

태태개발일지 - java 고급 멀티스레드(김영한)

태태코 2025. 3. 8. 14:08
반응형

 

전 글에 이어서 작성하겠다.

전 글을 요약하자면, 생산자와 소비자가 같은 대기공간에 들어가고 랜덤으로 깨어진다는 것이였다.

 

ReentrantLock

여기서 사용하는 것이 ReentrantLock 이다.

 

예시코드

    public void awaitMethod() {
        lock.lock();
        try {
            while (!ready) {  // ready가 true가 될 때까지 대기
                System.out.println(Thread.currentThread().getName() + " 대기 중...");
                condition.await(); // 현재 스레드를 대기 상태로 만든다.
            }
            System.out.println(Thread.currentThread().getName() + " 깨어남!");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            lock.unlock();
        }
    }
    
        public void signalMethod() {
        lock.lock();
        try {
            ready = true; // 대기 조건 변경
            System.out.println(Thread.currentThread().getName() + " 신호 보냄!");
            condition.signal(); // 대기 중인 스레드 하나를 깨운다.
        } finally {
            lock.unlock();
        }
    }

 

리엔트락에서 await()를 선언하면 대기상태로 들어가고, signal()를 보내면 깨는 형식이지만 여전히 하나의

Queue에서 꺼낸다면 그저 FIFO로 소비자와 생성자를 가리지 않고, 깨우는 상황이 일어난다.

 

그렇기 때문에 Condtion을 2개를 넣고 소비자와 생성자를 따로 구분해서 처리하면, 잘 작동하게 된다.

 

이렇게 ReentrantLock을 편리하게 사용하게 한 것이 Blocking Queue이다.

 

Blocking Queue

 

생산자

Thread producer = new Thread(() -> {
    try {
        for (int i = 1; i <= 10; i++) {
            System.out.println("생산자: " + i + " 추가");
            queue.put(i); // 큐가 가득 차면 블로킹됨
            Thread.sleep(500); // 생산 속도 조절
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
});

 

소비자

Thread consumer = new Thread(() -> {
    try {
        for (int i = 1; i <= 10; i++) {
            int item = queue.take(); // 큐가 비어 있으면 블로킹됨
            System.out.println("소비자: " + item + " 꺼냄");
            Thread.sleep(1000); // 소비 속도 조절
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
});

 

깨우기

이렇게 하면 소비자와 생산자를 어떻게 깨우는 가?

 

 

put 과 take의 메서드를 까보면

notEmpty일때의 시그널과 noutFull일때 시그널이 다르다.

 

생산자

lock.lock();
try {
    while (count == capacity) {   // 큐가 가득 찼으면 대기 (Blocking)
        notFull.await();          // 생산자 스레드가 대기 상태로 들어감
    }
    enqueue(e);                   // 큐에 데이터 추가
    count++;
    notEmpty.signal();             // ✅ 소비자 스레드 깨우기!
} finally {
    lock.unlock();
}

 

소비자

lock.lock();
try {
    while (count == 0) {         // 큐가 비어 있으면 대기 (Blocking)
        notEmpty.await();        // 소비자 스레드가 대기 상태로 들어감
    }
    e = dequeue();               // 큐에서 데이터 꺼내기
    count--;
    notFull.signal();            // ✅ 생산자 스레드 깨우기!
} finally {
    lock.unlock();
}

 

 

메서드 종류

메서드 설명  큐가 가득 찬 경우 큐가 비어 있는 경우
add(E e) 큐에 요소 추가 예외 발생 -
put(E e) 큐에 요소 추가 (블로킹) 대기 -
offer(E e) 큐에 요소 추가 (성공하면 true, 실패하면 false) 실패(false) -
offer(E e, long timeout, TimeUnit unit) 제한된 시간 내에 요소 추가 시도 시간 초과 후 실패 -
take() 큐에서 요소 가져오기 (블로킹) - 대기
poll() 큐에서 요소 가져오기 (즉시 반환) - null 반환
poll(long timeout, TimeUnit unit)

제한된 시간 내에 요소 가져오기 - 시간 초과 후 null

 

 

 

 

 

 

Blocking Queue가 사용되는 대표적인 경우

 

 1) 생산자-소비자(Producer-Consumer) 패턴

하나의 스레드(생산자)가 데이터를 생성하고, 다른 스레드(소비자)가 데이터를 소비하는 패턴.

BlockingQueue를 사용하면 동기화 코드를 직접 작성할 필요 없이 자연스럽게 구현 가능.

 

 2) 로그 처리 시스템

로그를 큐에 저장하고, 별도의 로그 처리 스레드가 이를 하나씩 가져와 저장.

 

3) 쓰레드 풀(Thread Pool) 작업 큐

작업을 요청하는 스레드가 BlockingQueue에 작업을 추가하고, 워커 스레드가 이를 가져가 실행.

 

 

반응형