프로그래밍 언어/JAVA

wait(), notify(), notifyAll()을 이용한 스레드 동기화

· 코딩마이데이

wait()-notify()를 이용한 스레드 동기화가 필요한 경우

스레드들이 sybchronized를 이용하여 공유 데이터에 순차적으로 잘 접근하도록 만들어진 경우라도, 여전히 동기화가 필요한 상황이 있습니다. 대표적 경우가 공유 메모리를 통해 두 스레드가 데이터를 주고받을 때, 공유 메모리에 대해 두 스레드가 동시에 접근하는 producer-cunsumer 문제입니다.

비디오 스트리밍 응용에서 입력 스레드와 재생 스레드의 버퍼 사용에 대한 동기화

 

Object의 wait(), notify() 메소드

wait()-notify()를 이용하면 앞의 producer-consumer 문제의 스레드 동기화를 해결할 수 있습니다. java.lang.Object 클래스는 스레드 사이에 동기화를 위한 3개의 메서드 wait(), notify, notifyAll()를 제공합니다. 모든 객체가 동기화 객체가 될 수 있도록 설계하였습니다.

 

wait() - 다른 스레드가 이 객체의 notify()를 불러줄 때까지 대기합니다.

notify() - 이 객체에 대기 중인 스레드를 깨워 RUUNABLE 상태로 만듭니다. 2개 이상의 스레드가 대기 중이라도 오직 한 개의 스레드만 깨워 RUUNABLE 상태로 합니다.

notifyAll() - 이 객체에 대기 중인 스레드를 깨우고 모두 RUNNABLE 상태로 합니다.

 

wait(), notify(), notifyAll()이 호출되는 여러 경우는 다음과 같습니다. 그림에서 ObjectS는 멀티스레드가 공유하는 객체입니다. 스레드가 ObjectS에 대해 락(lock)을 소유하면 문제없이 실행됩니다. 이미 다른 스레드가 락을 소유하고 있다면 ObjectS.wait()을 호출하여 락을 소유한 스레드가 ObjectS.notify()나 ObjectS.notifyAll()을 호출하여 깨워줄 때까지 기다립니다.

두 스래드가 ObjectS 객체에 동시 접근하는 경우, wait()와 notify

Thread A가 먼저 ObjectS의 data에 접근하고 있을 때 Thread B는 ObjectS.wait()을 호출하여 기다리고 있는 경우입니다. Thread A가 data를 가지고 처리할 작업을 끝내면 ObjectS.notify()를 호출하여 Thread B를 깨웁니다. 이제 Thread B는 data를 가지고 필요한 작업을 수행하게 됩니다.

여러 스레드가 Objects 객체에 동시 접근하는 경우, wait()과 notify()

 

Thread A가 먼저 ObjectS의 data에 접근하고 있을 때 Thread B, Thread C, Thread D, Thread E가 도착하여 모두 각각 ObjectS.wait()을 호출하여 기다리고 있는 경우입니다. Thread A가 data를 가지고 처리할 작업을 끝내면 ObjectS.notify()를 호출합니다. 그러면 JVM은 Thread B, Thread C, Thread D, Thread E 중에 하나만 깨웁니다. 깨어난 스레드는 이제 data를 가지고 필요한 작업을 수행하게 됩니다. 4개의 스레드 중에서 어떤 스레드를 깨울 것인지는 JVM이 결정합니다.

 

여러 스레드가 Objects 객체에 동시 접근하는 경우, wait()과 notifyAll()

 

Thread A가 먼저 ObjectS의 data에 접근하고 있을 때 Thread B, Thread C, Thread D, Thread E가 도착하여 모두 각각 ObjectS.wait()을 호출하여 기다리고 있는 상황이다. Thread A가 data를 가지고 처리할 작업을 끝내면 ObjectS.notifyAll()을 호출하여 Thread B, Thread C, Thread D, Thread E 모두 깨운다. 깨어난 각 스레드는 이제 data를 가지고 필요한 작업을 수행하게 된다. 그러나 조심해야 할 것은 또다시 깨어나 스레드들 사이에서 data에 대한 충돌이 생길 수 있다는 것이다. 그러므로 깨어난 스레드 중 한 개의 스레드만 data를 소유하고 나머지는 다시 wait()을 호출하여 잠을 자도록 잘 코딩해야 한다. 

 

wait(), notify()를 이용한 바 채우기

 

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

class MyLabel extends JLabel {
    private int barSize = 0; // 현재 그려져야할 바의 크기
    private int maxBarSize; // 바의 최대 크

    public MyLabel(int maxBarSize) {
        this.maxBarSize = maxBarSize;
    }

    @Override
    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        g.setColor(Color.MAGENTA);
        int width = (int) (((double)(this.getWidth()))/maxBarSize*barSize);
        if(width==0) return; // 크기가 0이기 때문에 바를 그릴 필요 없음
        g.fillRect(0, 0, width, this.getHeight());
    }

    synchronized public void fill() {
        if(barSize == maxBarSize) {
            try {
                wait(); // 바의 크기가 최대이면, ComsumerThread에 의해 바의 크기가 줄어들때까지 대기
            } catch (InterruptedException e) { return; }
        }
        barSize++;
        repaint(); // 바의 크기가 변했으니 다시 그리기
        notify(); // 기다리는 ConsumerThread 스레드 깨우기
    }
    synchronized public void consume() {
        if(barSize == 0) {
            try {
                wait(); // 바의 크기가 0이면 바의 크기가 0보다 커질 때까지 대기
            } catch (InterruptedException e) { return; }
        }
        barSize--;
        repaint(); // 바의 크기가 변했으니 다시 그리기
        notify(); // 기다리는 이벤트 스레드 깨우기
    }
}

class ConsumerThread extends Thread {
    private MyLabel bar;

    public ConsumerThread(MyLabel bar) {
        this.bar = bar;
    }

    @Override
    public void run() {
        while(true) {
            try {
                sleep(200);
                bar.consume(); // 0.2초마다 바를 1씩 줄인다.
            } catch (InterruptedException e) { return; }
        }
    }
}

public class TabAndThreadEx extends JFrame {
    private MyLabel bar = new MyLabel(100); // 바의 최대 크기를 100으로 설정

    public TabAndThreadEx(String title) {
        super(title);
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        Container c = getContentPane();
        c.setLayout(null);
        bar.setBackground(Color.ORANGE);
        bar.setOpaque(true);
        bar.setLocation(20,  50);
        bar.setSize(300, 20); // 300x20 크기의 바
        c.add(bar);

        // 컨텐트팬에 키 이벤트 핸들러 등록
        c.addKeyListener(new KeyAdapter() {
            @Override
            public void keyPressed(KeyEvent e) {
                bar.fill(); // 키를 누를 때마다 바가 1씩 증가한다.
            }
        });
        setSize(350,200);
        setVisible(true);

        c.setFocusable(true);
        c.requestFocus(); // 컨텐트팬에게 키 처리권 부여
        ConsumerThread th = new ConsumerThread(bar); // 스레드 생성
        th.start(); // 스레드 시작
    }

    public static void main(String[] args) {
        new TabAndThreadEx("아무키나 빨리 눌러 바 채우기");
    }
}

 

[실행결과]

초기 화면
키를 반복하여 빨리 누른 화면

 

'프로그래밍 언어 > JAVA' 카테고리의 다른 글

툴바  (1) 2025.08.31
메뉴 만들기  (2) 2025.08.28
스레드 동기화(Thread Synchronization)  (3) 2025.08.22
스레드 종료  (4) 2025.08.19
스레드 생명 주기와 스케줄링  (4) 2025.08.16