Java

명품자바 프로그래밍의 기초: 13장

통촏하여주시옵소서 2024. 9. 17. 11:31

객체지향 13장은 멀티스레드 프로그래밍을 다룹니다. 자바에서 스레드를 생성하고 관리하는 방법, 스레드의 동작 원리, 그리고 멀티스레드의 활용 사례들을 중심으로 설명하고 있습니다.

자바 멀티스레드 프로그래밍 – 스레드 관리와 동기화

1. 멀티스레딩 개념

멀티태스킹은 하나의 프로그램이 여러 작업(태스크)을 동시에 처리하는 것을 말합니다. 자바에서는 멀티태스킹을 멀티스레딩을 통해 구현할 수 있습니다. 멀티스레딩은 하나의 프로그램이 여러 개의 스레드를 실행해 동시에 여러 작업을 처리할 수 있게 합니다.

2. 스레드 생성 방법

자바에서 스레드를 생성하는 방법은 두 가지가 있습니다:

  1. hread 클래스 상속
  2. Runnable 인터페이스 구현

Thread 클래스 상속

Thread 클래스를 상속받아 run() 메소드를 오버라이딩하여 스레드의 동작을 정의할 수 있습니다. start() 메소드를 호출하여 스레드를 실행합니다.

class TimerThread extends Thread {  
    public void run() {  
        int n = 0;  
        while (true) {  
            System.out.println(n);  
            n++;  
            try {  
                Thread.sleep(1000); // 1초 대기  
            } catch (InterruptedException e) {  
                return;  
            }  
        }  
    }  
}

public class ThreadExample {  
    public static void main(String[] args) {  
        TimerThread th = new TimerThread();  
        th.start(); // 스레드 실행  
    }  
}

2. Runnable 인터페이스 구현

Runnable 인터페이스를 구현한 후, Thread 객체를 생성할 때 해당 Runnable 객체를 넘겨주는 방법입니다.

class TimerRunnable implements Runnable {  
    public void run() {  
        int n = 0;  
        while (true) {  
            System.out.println(n);  
            n++;  
            try {  
                Thread.sleep(1000); // 1초 대기  
            } catch (InterruptedException e) {  
                return;  
            }  
        }  
    }  
}

public class RunnableExample {  
    public static void main(String[] args) {  
        Thread th = new Thread(new TimerRunnable());  
        th.start(); // 스레드 실행  
    }  
}

3. 스레드 동작과 상태

스레드는 생명 주기를 가지며, 다양한 상태를 거칩니다:

  • NEW: 스레드가 생성되었지만 아직 실행되지 않은 상태
  • RUNNABLE: 스레드가 실행 중이거나 실행 대기 중인 상태
  • WAITING: 스레드가 다른 스레드의 동작을 기다리는 상태
  • TIMED WAITING: 일정 시간 동안 대기하는 상태
  • BLOCKED: I/O 작업 등으로 인해 대기 중인 상태
  • TERMINATED: 스레드가 종료된 상태

4. 스레드 동기화

멀티스레드 환경에서는 여러 스레드가 공유 자원에 접근할 때 동기화가 필요합니다. 자바는 synchronized 키워드를 사용해 한 번에 한 스레드만 공유 자원에 접근하도록 할 수 있습니다.

예제: 동기화 적용

class SharedBoard {  
    private int sum = 0;

    synchronized public void add() {  
        int n = sum;  
        n += 10;  
        sum = n;  
        System.out.println(Thread.currentThread().getName() + " : " + sum);  
    }  

    public int getSum() {  
        return sum;  
    }  
}

class StudentThread extends Thread {  
    private SharedBoard board;

    public StudentThread(String name, SharedBoard board) {  
        super(name);  
        this.board = board;  
    }  

    public void run() {  
        for (int i = 0; i < 10; i++) {  
            board.add();  
        }  
    }  
}

public class SynchronizedExample {  
    public static void main(String[] args) {  
        SharedBoard board = new SharedBoard();  
        Thread th1 = new StudentThread("학생1", board);  
        Thread th2 = new StudentThread("학생2", board);  
        th1.start();  
        th2.start();  
    }  
}

이 예제에서는 두 스레드가 SharedBoard 객체의 add() 메소드를 동시에 호출할 수 없도록 synchronized 키워드를 사용하여 동기화를 구현했습니다.

5. 스레드 강제 종료와 interrupt()

스레드는 interrupt() 메소드를 사용해 다른 스레드를 강제 종료시킬 수 있습니다. InterruptedException을 처리하여 스레드를 안전하게 종료하는 방법도 있습니다.

class TimerThread extends Thread {  
    public void run() {  
        int n = 0;  
        while (true) {  
            System.out.println(n);  
            n++;  
            try {  
                Thread.sleep(1000); // 1초 대기  
            } catch (InterruptedException e) {  
                return; // 예외 발생 시 스레드 종료  
            }  
        }  
    }  
}

public class InterruptExample {  
    public static void main(String[] args) {  
        TimerThread th = new TimerThread();  
        th.start();  
        try {  
            Thread.sleep(5000); // 5초 대기  
        } catch (InterruptedException e) {}  
        th.interrupt(); // 스레드 강제 종료  
    }  
}

6. wait(), notify(), notifyAll() 메소드

자바에서 스레드 간의 협력을 위해 wait(), notify(), notifyAll() 메소드를 사용하여 스레드를 일시적으로 대기시키고, 다른 스레드가 이를 깨울 수 있습니다. 이러한 메소드들은 반드시 synchronized 블록 내에서 사용되어야 합니다.

wait() 메소드

  • wait() 메소드는 현재 스레드를 일시 정지 상태로 만듭니다. 스레드는 notify() 또는 notifyAll() 메소드가 호출될 때까지 대기 상태에 머뭅니다.
  • 이 메소드는 주로 공유 자원을 다른 스레드가 사용할 때 대기하는 스레드에게 사용됩니다.

notify() 메소드

  • notify() 메소드는 wait() 상태에서 대기 중인 스레드 중 하나를 깨워서 RUNNABLE 상태로 전환시킵니다.
  • notify()는 한 번에 하나의 스레드만 깨울 수 있습니다.

notifyAll() 메소드

  • notifyAll()은 대기 중인 모든 스레드를 깨웁니다. 이 메소드를 사용하면 여러 스레드가 wait() 상태에서 동시에 RUNNABLE 상태로 전환됩니다.

7. wait(), notify(), notifyAll() 메소드 예제 코드

다음은 wait(), notify(), notifyAll() 메소드를 사용하여 스레드 간 협력을 구현한 예제입니다. 이 예제에서는 두 스레드가 바를 채우고 소비하는 협력 작업을 수행합니다.

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

class MyLabel extends JLabel {
    private int barSize = 0;  // 바 크기
    private int maxBarSize;

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

    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        g.setColor(Color.MAGENTA);
        int width = (int)(((double)(this.getWidth())) / maxBarSize * barSize);
        if (width == 0) return;
        g.fillRect(0, 0, width, this.getHeight());
    }

    // 바를 채우는 메소드
    synchronized void fill() {
        if (barSize == maxBarSize) {
            try {
                wait();  // 바가 꽉 찼으면 대기
            } catch (InterruptedException e) {
                return;
            }
        }
        barSize++;  // 바를 채움
        repaint();
        notify();  // 대기 중인 스레드 깨움
    }

    // 바를 소비하는 메소드
    synchronized void consume() {
        if (barSize == 0) {
            try {
                wait();  // 바가 비었으면 대기
            } catch (InterruptedException e) {
                return;
            }
        }
        barSize--;  // 바를 소비함
        repaint();
        notify();  // 대기 중인 스레드 깨움
    }
}

class ConsumerThread extends Thread {
    private MyLabel bar;

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

    public void run() {
        while (true) {
            try {
                sleep(200);  // 0.2초 대기
                bar.consume();  // 바 소비
            } catch (InterruptedException e) {
                return;
            }
        }
    }
}

public class WaitNotifyExample extends JFrame {
    private MyLabel bar = new MyLabel(100);  // 최대 100 크기의 바

    public WaitNotifyExample(String title) {
        super(title);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        Container c = getContentPane();
        c.setLayout(null);
        bar.setBackground(Color.ORANGE);
        bar.setOpaque(true);
        bar.setSize(300, 20);
        bar.setLocation(20, 50);
        c.add(bar);

        // 키 입력으로 바 채우기
        c.addKeyListener(new KeyAdapter() {
            public void keyPressed(KeyEvent e) {
                bar.fill();
            }
        });

        setSize(350, 200);
        setVisible(true);
        c.requestFocus();  // 포커스 요청

        ConsumerThread th = new ConsumerThread(bar);  // 소비 스레드 실행
        th.start();
    }

    public static void main(String[] args) {
        new WaitNotifyExample("바 채우기 예제");
    }
}

 

8. 코드 설명

  1. MyLabel 클래스
    • MyLabel은 바를 그리기 위한 레이블입니다. fill() 메소드를 통해 바를 채우고, consume() 메소드를 통해 바를 소비합니다.
    • synchronized 블록을 사용하여 한 번에 하나의 스레드만 fill() 또는 consume() 작업을 수행할 수 있도록 동기화합니다.
    • wait() 메소드는 바가 꽉 차면 대기하고, notify() 메소드는 바가 채워지거나 소비된 후 다른 스레드를 깨웁니다.
  2. ConsumerThread 클래스
    • ConsumerThread는 바를 소비하는 스레드입니다. 0.2초마다 consume() 메소드를 호출하여 바를 하나씩 소비합니다.
    • 바가 비어있을 경우, wait() 상태에서 대기하다가 fill() 메소드가 호출되면 깨워져 다시 바를 소비합니다.
  3. WaitNotifyExample 클래스
    • WaitNotifyExample은 JFrame을 상속받아 GUI를 구성하고, 키보드를 눌렀을 때 fill() 메소드를 호출하여 바를 채웁니다.
    • ConsumerThread를 실행시켜 바를 계속해서 소비하도록 만듭니다.

마무리

이 코드에서는 wait(), notify(), notifyAll() 메소드를 통해 두 스레드 간의 협력을 구현했습니다. wait()는 스레드가 조건이 충족될 때까지 대기 상태로 만들며, notify()는 한 스레드를 깨우고, notifyAll()은 모든 대기 중인 스레드를 깨웁니다. 이 메소드는 스레드 간의 효율적인 협력을 위해 필수적인 개념입니다.