Object notify(), wait()

2024. 10. 6. 23:26카테고리 없음

wait() 현재 스레드가 가진 락을 반납하고 대기(WAITING)한다. 현재 스레드가 syncrhonized 블록이나 메서드에서 락을 소유하고 있을 때만 호출할 수 있다. wait()를 호출한 스레드는 락을 반납하고, 다른 스레드가 해당 락을 획득할 수 있도록 한다. 이렇게 대기 상태로 전환된 스레드는 다른 스레드가 notify() 또는 notifyAll() 을 호출할 때까지 대기 상태를 유지한다.

notify() 대기 중인 스레드 하나를 깨운다. 이 메서드는 synchronized 블록이나 메서드에서 사용이 되어야 한다. 깨어난 스레드는 WAITING → BLOCKED 상태가 된다.

  • BLOCKED 상태는 락 대기 집합이라는 곳에서 관리가 된다.

notifyAll() 대기 중인 모든 스레드를 깨운다.

wait()과 notify()의 한계

  • 어떤 순서로 대기 상태의 스레드를 깨우는지 알 수 없어 스레드 기아가 나타날 수 있다. notifyAll()을 사용하면 되지만 비효율적이다.(이론적으로)
  • 원하는 스레드를 깨울 수 없다.

따라서 각각의 스레드 대기 집합을 Lock과 ReentrantLock 을 이용한다. 아래 As-is와 To-be는 같다. 왜냐면

As-is

@Override
public synchronized void put(String data) {
    while (queue.size() == max) {
        log("[put] 큐가 가득 참, 생산자 대기");
        try {
            wait(); // RUNNABLE -> WAITING, 락 반납
            log("[put] 생산자 깨어남");
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
    queue.offer(data);
    log("[put] 생산자 데이터 저장, notify() 호출");
    notify(); // 대기 스레드, WAIT -> BLOCKED
}

1차 To-be : 스레드 대기 공간을 분리하지 않았기 때문에 As-is와 같은 동작을 한다.

private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
    
@Override
public void put(String data) {
    lock.lock();
    try {
        while (queue.size() == max) {
            log("[put] 큐가 가득 참, 생산자 대기");
            try {
                condition.await();
                log("[put] 생산자 깨어남");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        queue.offer(data);
        log("[put] 생산자 데이터 저장, signal() 호출");
        condition.signal();
    } finally {
        lock.unlock();
    }

}

@Override
public String take() {
    lock.lock();
    try {
        while (queue.isEmpty()) {
            log("[take] 큐에 데이터가 없음, 소비자 대기");
            try {
                condition.await();
                log("[take] 소비자 깨어남");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        String data = queue.poll();
        log("[take] 소비자 데이터 획득, signal() 호출");
        condition.signal();
        return data;
    } finally {
        lock.unlock();
    }
}

2차 To-be

private final Lock lock = new ReentrantLock();
private final Condition producerCondition = lock.newCondition();
private final Condition consumerCondition = lock.newCondition();

  
@Override
public void put(String data) {
    lock.lock();
    try {
        while (queue.size() == max) {
            log("[put] 큐가 가득 참, 생산자 대기");
            try {
                producerCondition.await();
                log("[put] 생산자 깨어남");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        queue.offer(data);
        log("[put] 생산자 데이터 저장, consumerCond.signal() 호출");
        consumerCondition.signal();
    } finally {
        lock.unlock();
    }

}

@Override
public String take() {
    lock.lock();
    try {
        while (queue.isEmpty()) {
            log("[take] 큐에 데이터가 없음, 소비자 대기");
            try {
                consumerCondition.await();
                log("[take] 소비자 깨어남");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        String data = queue.poll();
        log("[take] 소비자 데이터 획득, producerCond.signal() 호출");
        producerCondition.signal();
        return data;
    } finally {
        lock.unlock();
    }
}

자바의 모든 객체는 멀티 스레드와 임계 영역을 다루기 위해 다음 3가지를 내부에 기본 요소로 가진다. synchronized 키워드를 쓰면 아래 영역들을 사용하게 된다.

  • 모니터 락
  • 락 대기 집합(모니터 락 대기 집합)
  • 스레드 대기 집합

ChatGPT 설명

자바에서 **synchronized** 키워드와 관련된 중요한 개념으로 모니터 락(Monitor Lock), 모니터 락 대기 집합(Monitor Lock Waiting Set), 그리고 스레드 대기 집합(Thread Waiting Set)이 있습니다. 이들은 모두 자바에서 멀티스레딩 환경에서 스레드 간 동기화를 처리할 때 매우 중요한 역할을 합니다. 각각에 대해 자세히 알아보겠습니다.


1. 모니터 락 (Monitor Lock)

모니터 락은 객체 단위로 존재하며, 자바의 모든 객체는 자동으로 모니터를 가지고 있습니다. 모니터는 특정 객체에 대한 동기화 제어를 의미하며, 자바에서 **synchronized** 블록이나 메서드를 사용할 때 이 모니터 락이 중요한 역할을 합니다. 모니터 락은 특정 객체에 대해 하나의 스레드만이 접근할 수 있게 보장하는 메커니즘입니다.

모니터 락의 동작 방식:

  • 하나의 스레드가 동기화된 블록에 들어가기 위해서는 해당 객체의 모니터 락을 획득해야 합니다.
  • 객체에 이미 다른 스레드가 모니터 락을 소유하고 있으면, 다른 스레드는 락이 해제될 때까지 대기 상태로 머물러야 합니다.
  • 모니터 락을 얻은 스레드는 해당 락이 걸린 코드 블록을 실행하고, 실행이 끝나면 락을 해제합니다.
java
코드 복사
public class Example {
    public synchronized void syncMethod() {
        // 현재 객체(this)에 대해 모니터 락을 잡음
        // 락을 잡은 스레드만 이 코드를 실행할 수 있음
    }
}

위 코드에서 syncMethod는 synchronized 키워드를 사용했기 때문에 해당 메서드에 들어가려면 해당 객체의 모니터 락을 획득해야만 합니다.


2. 모니터 락 대기 집합 (Monitor Lock Waiting Set)

모니터 락 대기 집합모니터 락을 얻기 위해 기다리는 스레드들이 모여 있는 집합입니다. 스레드가 synchronized 블록에 들어가려고 할 때, 만약 이미 다른 스레드가 해당 객체의 모니터 락을 소유하고 있다면, 그 스레드는 모니터 락 대기 집합에 들어가서 락이 해제될 때까지 기다립니다.

모니터 락 대기 집합의 동작 방식:

  • 스레드는 특정 객체에 대해 synchronized 블록이나 메서드에 접근하려고 할 때 해당 객체의 모니터 락을 요구합니다.
  • 만약 다른 스레드가 이미 해당 모니터 락을 소유하고 있다면, 스레드는 모니터 락 대기 집합에 추가되고 락이 해제될 때까지 대기합니다.
  • 모니터 락을 소유한 스레드가 락을 해제하면, 대기 집합에 있는 스레드 중 하나가 락을 획득하여 블록에 들어갈 수 있습니다. 이때, 스레드가 어떤 순서로 선택되는지는 JVM의 구현에 따라 다를 수 있습니다.
java
코드 복사
public class Example {
    private final Object lock = new Object();

    public void syncBlock() {
        synchronized(lock) {
            // lock 객체에 대해 모니터 락을 얻음
            // 다른 스레드들은 모니터 락 대기 집합에서 대기
        }
    }
}

위 예시에서, 만약 lock 객체에 대해 다른 스레드가 락을 잡고 있다면, 다른 스레드들은 모니터 락 대기 집합에 들어가 락이 해제되기를 기다리게 됩니다.


3. 스레드 대기 집합 (Thread Waiting Set)

스레드 대기 집합은 Object.wait() 메서드를 호출한 스레드들이 모여 있는 집합입니다. 이 집합은 wait()를 호출한 스레드가 해당 객체의 모니터 락을 일시적으로 포기하고 다른 스레드가 해당 락을 얻을 수 있도록 대기하는 상태입니다. 스레드가 대기 집합에서 깨어나기 위해서는 다른 스레드가 **notify()** 또는 **notifyAll()**을 호출해야 합니다.

스레드 대기 집합의 동작 방식:

  • 스레드가 특정 객체에 대해 wait() 메서드를 호출하면, 그 스레드는 모니터 락을 해제하고 스레드 대기 집합에 들어가게 됩니다.
  • 다른 스레드가 notify()를 호출하면, 대기 집합에 있는 스레드 중 하나가 모니터 락을 다시 얻을 수 있게 됩니다.
  • notifyAll()을 호출하면, 대기 집합에 있는 모든 스레드가 다시 모니터 락을 얻기 위해 경쟁하게 됩니다.
  • 단, 스레드가 깨어난 후에도 다시 모니터 락을 획득할 수 있을 때까지 기다려야 합니다.
java
코드 복사
public class Example {
    private final Object lock = new Object();

    public void waitingThread() throws InterruptedException {
        synchronized(lock) {
            lock.wait(); // 스레드는 lock 객체의 스레드 대기 집합에 들어감
        }
    }

    public void notifyingThread() {
        synchronized(lock) {
            lock.notify(); // 대기 집합에서 대기 중인 스레드 중 하나를 깨움
        }
    }
}

위 예시에서 waitingThread() 메서드를 호출한 스레드는 wait()를 호출하여 스레드 대기 집합으로 들어가고, notifyingThread() 메서드에서 notify()를 호출하여 해당 대기 중인 스레드 중 하나를 다시 실행하게 합니다.


요약

  1. 모니터 락 (Monitor Lock): 특정 객체에 대해 하나의 스레드만이 synchronized 블록 또는 메서드에 접근할 수 있도록 보장하는 메커니즘.
  2. 모니터 락 대기 집합 (Monitor Lock Waiting Set): 모니터 락을 얻기 위해 대기 중인 스레드들이 모여 있는 집합. 락이 해제되면 대기 중인 스레드 중 하나가 락을 얻음.
  3. 스레드 대기 집합 (Thread Waiting Set): wait()를 호출한 스레드들이 들어가는 집합. 스레드는 notify() 또는 notifyAll() 호출에 의해 다시 실행될 수 있음.

이러한 개념들은 자바에서 스레드 간 동기화 및 통신을 제어하는 데 중요한 역할을 합니다. synchronized 키워드를 통해 안전하게 공유 자원을 관리하고, wait()와 notify()를 통해 스레드 간의 효율적인 통신을 구현할 수 있습니다.

라이브러리로 제공되는 Lock은 코드 레벨에서 락과 스레드 대기를 관리하는데 결국 위의 3가지 기본 요소를 코드 레벨에서 자체적으로 가지고 있다.

Lock 인터페이스는 synchronized 키워드와 유사한 기능을 제공하지만, 더 세밀한 제어와 다양한 락의 구현을 가능하게 하는 자바의 동시성 유틸리티입니다. Lock 인터페이스는 직접적으로 모니터 락, 모니터 락 대기 집합, 스레드 대기 집합을 사용하는 것이 아니라, 코드 레벨로 구현된 것입니다. 즉, Lock 인터페이스와 관련된 클래스들은 내부적으로 이러한 요소를 사용하여 동기화 메커니즘을 구현하고, 개발자는 더 높은 수준에서 락을 사용할 수 있게 합니다.

Lock 인터페이스와 synchronized의 차이점

기본적으로 Lock 인터페이스는 synchronized 블록과 매우 유사한 개념을 제공합니다. 하지만 synchronized 키워드는 자바 언어 수준에서 제공되며, 기본적으로 JVM에 의해 관리되는 모니터 락, 대기 집합 등을 사용합니다. 반면, Lock 인터페이스는 자바 라이브러리(java.util.concurrent.locks 패키지)에 의해 제공되며, 더 세밀한 동작 제어를 허용합니다.

Lock 인터페이스를 사용하면:

  1. 더 세밀한 제어: 락을 획득하고 해제하는 시점을 명시적으로 관리할 수 있습니다.
  2. 락의 공정성: 공정한 락(fair lock)을 제공할 수 있어 스레드가 락을 얻는 순서를 제어할 수 있습니다.
  3. 타임아웃: 일정 시간이 지나면 락 획득을 포기할 수 있습니다.
  4. 다양한 락 구현: 여러 종류의 락을 사용할 수 있습니다. 예를 들어, 읽기/쓰기 락(ReentrantReadWriteLock)을 통해 읽기 락과 쓰기 락을 구분해서 사용할 수 있습니다.

Lock 인터페이스의 주요 메서드

Lock 인터페이스는 다음과 같은 메서드를 제공합니다:

  • lock(): 락을 획득할 때까지 무한히 대기합니다.
  • unlock(): 락을 해제합니다.
  • tryLock(): 락을 즉시 시도하고, 성공하면 true를 반환하며 실패하면 false를 반환합니다.
  • tryLock(long time, TimeUnit unit): 주어진 시간 내에 락을 시도하고, 성공 여부에 따라 true 또는 false를 반환합니다.
  • lockInterruptibly(): 스레드가 중단될 수 있도록 락을 획득할 수 있습니다.

Lock 인터페이스와 세 가지 요소의 관계

  1. 모니터 락
    • Lock 인터페이스는 synchronized 키워드처럼 모니터 락의 개념을 제공합니다. 즉, lock() 메서드를 호출하면 해당 락을 가진 스레드만 임계 구역에 접근할 수 있게 됩니다. 다만, synchronized와는 다르게 명시적으로 lock()과 unlock() 메서드를 호출하여 락을 제어할 수 있습니다.
  2. 모니터 락 대기 집합
    • 모니터 락 대기 집합은 락을 얻기 위해 기다리는 스레드들의 집합입니다. Lock 인터페이스를 사용하는 경우에도, lock() 메서드가 호출되면, 만약 다른 스레드가 이미 락을 소유하고 있다면 현재 스레드는 락을 획득할 수 있을 때까지 기다리게 됩니다. 이는 내부적으로 JVM이 제공하는 대기 메커니즘을 사용하거나, Lock 인터페이스 구현체가 관리하는 대기 큐에 의해 처리됩니다. (synchonized와는 다르게 WAITING 상태로 가지고 있음, synchronized는 BLOCKED 상태)
  3. 스레드 대기 집합예를 들어, Condition을 사용하면 특정 조건을 만족할 때까지 스레드가 기다리도록 할 수 있으며, 특정 조건이 만족되었을 때 다른 스레드에게 신호를 보내어 대기 중인 스레드를 깨울 수 있습니다.
    • Condition 인터페이스는 Object의 wait()/notify()와 유사한 역할을 합니다. Lock 인터페이스는 스레드 간의 통신을 위해 별도의 스레드 대기 집합을 관리하는데, 이를 위해 Condition을 사용합니다. Condition은 await(), signal(), signalAll()과 같은 메서드를 제공하여 Lock 인터페이스와 함께 사용할 수 있습니다.

Lock 인터페이스 사용 예시

java
코드 복사
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockExample {
    private final Lock lock = new ReentrantLock();

    public void performTask() {
        lock.lock(); // 락 획득
        try {
            // 임계 구역
            System.out.println("Task is being performed");
        } finally {
            lock.unlock(); // 락 해제
        }
    }
}

위 예시에서 lock.lock()을 호출하여 락을 획득하고, 임계 구역을 실행한 후 finally 블록에서 락을 해제합니다. 이는 synchronized 키워드와 유사한 방식으로 동작하지만, lock()과 unlock()을 통해 더 명시적으로 락을 제어할 수 있습니다.

요약

  • Lock 인터페이스는 직접적으로 모니터 락, 모니터 락 대기 집합, 스레드 대기 집합을 사용하지 않고, 내부적으로 이러한 동작을 코드로 구현하여 개발자에게 더 많은 제어권을 제공합니다.
  • Lock 인터페이스는 더 세밀한 동기화 제어를 가능하게 하며, 스레드가 락을 얻고 해제하는 과정을 명시적으로 관리할 수 있습니다.
  • Condition 인터페이스를 사용하여 스레드 간의 대기 및 신호 보내기를 관리할 수 있으며, 이는 wait()/notify()와 유사한 방식으로 동작합니다.

따라서 Lock 인터페이스는 내부적으로 이러한 동기화 메커니즘을 관리하는 구현체이며, 개발자는 이를 사용하여 더 유연하고 세밀한 동시성 제어를 할 수 있습니다.