티스토리 뷰

Language/Java

멀티스레드 프로그래밍

Seogineer 2022. 10. 19. 17:36

스레드(thread)란?

명령문이 순서대로 하나씩 처리되는 것. 즉, 프로그램의 실행 흐름.

 

 

멀티스레드 프로그램(multi-thread program)이란?

둘 이상의 실행 흐름을 갖는 프로그램.

 

멀티스레드 프로그램의 작동 방식

  • 메인 스레드만 프로그램이 시작되면 자동으로 시작되고, 다른 스레드들은 메인 스레드에서 만들어서 시작한다.
  • 메인 스레드가 끝나더라도 다른 스레드는 끝나지 않고 실행을 계속할 수 있다.
  • 스레드는 동시에 실행되는 것이 아니라 자바 가상 머신이 스레드를 번갈아 실행한다.

 

멀티스레드 프로그램의 작성 방법

1. Thread 클래스를 이용하는 방법

class AlphabetThread extends Thread {
    public void run () {
        for (char ch = 'A'; ch <= 'Z'; ch++) {
            System.out.print(ch);
            try {
            	Thread.sleep(100);
            } catch (InterruptedException e) {
            	e.printStackTrace();
            }
        }
    }
}
class DigitThread extends Thread {
    public void run() {
        for (int cnt = 0; cnt < 10; cnt++) {
            System.out.print(cnt);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
class Main {
    public static void main(String[] args) {
        Thread thread1 = new DigitThread();
        Thread thread2 = new AlphabetThread();
        thread1.start();
        thread2.start();
    }
}

 

2. Runnable 인터페이스를 이용하는 방법

class KoreanLetters implements Runnable {
    public void run(){
        char[] arr = {'ㄱ', 'ㄴ', 'ㄷ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅅ', 'ㅇ', 'ㅈ', 'ㅊ', 'ㅌ', 'ㅍ', 'ㅎ'};
        for(char ch : arr) {
            System.out.print(ch);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
class SmallLetters implements Runnable {
    public void run () {
        for (char ch = 'a'; ch <= 'z'; ch++){
            System.out.print(ch);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
class Main {
    public static void main(String[] args) {
        SmallLetters obj1 = new SmallLetters();
        KoreanLetters obj2 = new KoreanLetters();
        Thread thread1 = new Thread(obj1);
        Thread thread2 = new Thread(obj2);
        thread1.start();
        thread2.start();
    }
}

 

스레드간의 커뮤니케이션

스레드간의 데이터 교환

공유 영역을 만들어서 여러 스레드들이 데이터를 교환할 수 있다.
class SharedArea {
    double result;
    volatile boolean isReady;
    // 자바 컴파일러는 성능 향상을 위해서 cpu cache에 값을 가져다 놓고 사용하는데, 
    // 그렇게 되면 최초에 가져온 false가 true로 바뀌지 않아서 while 문이 무한 루프에 빠지게 된다.
    // volatile을 선언하여 cpu cache가 아니라 main memory에서 값을 읽어온다.
}
class CalcThread extends Thread {
    SharedArea sharedArea;
    public void run(){
        double total = 0.0;
        for(int cnt = 1; cnt < 1000000000; cnt += 2)
            if (cnt / 2 % 2 == 0)
                total += 1.0 / cnt;
            else
                total -= 1.0 / cnt;
        sharedArea.result = total * 4;
        sharedArea.isReady = true;  //작업이 끝난 상태를 전달
    }
}
class PrintThread extends Thread {
    SharedArea sharedArea;
    public void run () {
        while (sharedArea.isReady != true) {
            continue;
        }
        System.out.println(sharedArea.result);
    }
}
class Main {
    public static void main(String[] args) {
        CalcThread thread1 = new CalcThread();
        PrintThread thread2 = new PrintThread();
        SharedArea obj = new SharedArea();
        thread1.sharedArea = obj;
        thread2.sharedArea = obj;
        thread1.start();
        thread2.start();
    }
}

 

critical section의 동기화

스레드가 적절치 못한 순간에 다른 스레드로 제어가 넘어가는 것을 방지할 수 있다.
class SharedArea {
    Account account1;
    Account account2;
}
class TransferThread extends Thread {
    SharedArea sharedArea;
    TransferThread (SharedArea area) {
        sharedArea = area;
    }
    public void run() {
        for(int cnt = 0; cnt < 12; cnt++) {
            //critical section start
            sharedArea.account1.withdraw(1000000);
            System.out.print("이몽룡 계좌: 100만원 인출, ");
            sharedArea.account2.deposit(1000000);
            System.out.println("성춘향 계좌: 100만원 입금");
            //critical section end
        }
    }
}
class PrintThread extends Thread {
    SharedArea sharedArea;
    PrintThread (SharedArea area) {
        sharedArea = area;
    }
    public void run () {
        for(int cnt = 0; cnt < 3; cnt++){
            // critical section start
            int sum = sharedArea.account1.balance +
                      sharedArea.account2.balance;
            System.out.println("계좌 잔액 합계: " + sum);
            // critical section end
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                System.out.println(e.getMessage());
            }
        }
    }
}
class Account {
    String accountNo;
    String ownerName;
    int balance;
    Account (String accountNo, String ownerName, int balance) {
        this.accountNo = accountNo;
        this.ownerName = ownerName;
        this.balance = balance;
    }
    void deposit (int amount) {
        balance += amount;
    }
    int withdraw (int amount) {
      if (balance < amount)
          return 0;
      balance -= amount;
      return amount;
    }
}
class Main {
    public static void main(String[] args) {
        SharedArea area = new SharedArea();
        area.account1 = new Account("111-111-1111", "이몽룡", 20000000);
        area.account2 = new Account("222-222-2222", "성춘향", 10000000);
        TransferThread thread1 = new TransferThread(area);
        PrintThread thread2 = new PrintThread(area);
        thread1.start();
        thread2.start();
    }
}

 

이몽룡 계좌에서 100만원을 인출한 순간, 제어권이 PrintThread로 넘어가면서 잔액이 29000000로 나오는 현상이 발생

 

1. 동기화 블록(synchronized block)을 이용한 동기화 방법

synchronize (공유_객체) {
    critical section
}
class PrintThread extends Thread {
    SharedArea sharedArea;
    PrintThread (SharedArea area) {
        sharedArea = area;
    }
    public void run () {
        for(int cnt = 0; cnt < 3; cnt++){
            // 동기화 블록 시작
            synchronized (sharedArea) {
                int sum = sharedArea.account1.balance +
                          sharedArea.account2.balance;
                System.out.println("계좌 잔액 합계: " + sum);
            }
            // 동기화 블록 끝
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                System.out.println(e.getMessage());
            }
        }
    }
}
class TransferThread extends Thread {
    SharedArea sharedArea;
    TransferThread (SharedArea area) {
        sharedArea = area;
    }
    public void run(){
        for(int cnt = 0; cnt < 12; cnt++) {
            // 동기화 블록 시작
            synchronized (sharedArea) {
                sharedArea.account1.withdraw(1000000);
                System.out.print("이몽룡 계좌: 100만원 인출, ");
                sharedArea.account2.deposit(1000000);
                System.out.println("성춘향 계좌: 100만원 입금");   
            }
            // 동기화 블록 끝
        }
    }
}

 

2. 동기화 메소드를 이용한 동기화 방법

class SharedArea {
    Account account1;
    Account account2;
    // 동기화 메소드 시작
    synchronized void transfer(int amount) {
        account1.withdraw(amount);
        System.out.print("이몽룡 계좌: 100만원 인출, ");
        account2.deposit(amount);
        System.out.println("성춘향 계좌: 100만원 입금");   
    }
    // 동기화 메소드 끝
    // 동기화 메소드 시작
    synchronized int getTotal() {
        return account1.balance + account2.balance;
    }
    // 동기화 메소드 끝
}
class TransferThread extends Thread {
    SharedArea sharedArea;
    TransferThread (SharedArea area) {
        sharedArea = area;
    }
    public void run(){
        for(int cnt = 0; cnt < 12; cnt++) {
            sharedArea.transfer(1000000);  // 동기화 메소드 호출
        }
    }
}
class PrintThread extends Thread {
    SharedArea sharedArea;
    PrintThread (SharedArea area) {
        sharedArea = area;
    }
    public void run () {
        for(int cnt = 0; cnt < 3; cnt++){
            int sum = sharedArea.getTotal();  // 동기화 메소드 호출
            System.out.println("계좌 잔액 합계: " + sum);
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                System.out.println(e.getMessage());
            }
        }
    }
}

 

스레드간의 신호 전송

스레드간 직접 신호를 주고 받을 수 있다.

 

메소드 종류

  • notify() : 다른 스레드로 신호를 보내는 메소드
  • wait() : 다른 스레드로부터 신호가 오기를 기다리는 메소드
  • notifyAll() : wait 하고 있는 모든 스레드에게 신호를 보내는 메소드

 

notify()와 wait()의 사용 방법

class CalcThread extends Thread {
    SharedArea sharedArea;
    public void run(){
        double total = 0.0;
        for(int cnt = 1; cnt < 1000000000; cnt += 2)
            if (cnt / 2 % 2 == 0)
                total += 1.0 / cnt;
            else
                total -= 1.0 / cnt;
        sharedArea.result = total * 4;
        sharedArea.isReady = true;
        synchronized (sharedArea) {
            sharedArea.notify();  // 계산을 완료하면 신호를 보냄
        }
    }
}
class PrintThread extends Thread {
    SharedArea sharedArea;
    public void run () {
        if (sharedArea.isReady != true) {  // 반복적으로 상태를 검사해야 했던 while 문을 제거
            try {
                synchronized (sharedArea) {
                    sharedArea.wait();  // 신호를 받음
                }
            } catch (InterruptedException e) {
                System.out.println(e.getMessage());
            }
        }
        System.out.println(sharedArea.result);
    }
}

 

notifyAll()의 사용 방법

class CalcThread extends Thread {
    SharedArea sharedArea;
    public void run(){
        double total = 0.0;
        for(int cnt = 1; cnt < 1000000000; cnt += 2)
            if (cnt / 2 % 2 == 0)
                total += 1.0 / cnt;
            else
                total -= 1.0 / cnt;
        sharedArea.result = total * 4;
        sharedArea.isReady = true;
        synchronized (sharedArea) {
            sharedArea.notifyAll();  // 기다리는 모든 스레드로 신호를 보냄
        }
    }
}
class PrintThread extends Thread {
    SharedArea sharedArea;
    public void run () {
        if (sharedArea.isReady != true) {
            try {
                synchronized (sharedArea) {
                    sharedArea.wait();  // 신호를 기다림
                }
            } catch (InterruptedException e) {
                System.out.println(e.getMessage());
            }
        }
        System.out.println(sharedArea.result);
    }
}
class SimplePrintThread extends Thread {
    SharedArea sharedArea;
    public void run () {
        if (sharedArea.isReady != true) {
            try {
                synchronized (sharedArea) {
                    sharedArea.wait();  // 신호를 기다림
                }
            } catch (InterruptedException e) {
                System.out.println(e.getMessage());
            }
        }
        System.out.printf("%.2f %n", sharedArea.result);
    }
}
class LuxuryPrintThread extends Thread {
    SharedArea sharedArea;
    public void run () {
        if (sharedArea.isReady != true) {
            try {
                synchronized (sharedArea) {
                    sharedArea.wait();  // 신호를 기다림
                }
            } catch (InterruptedException e) {
                System.out.println(e.getMessage());
            }
        }
        System.out.println("*** pi = " + sharedArea.result + " ***");
    }
}

실행 결과

 

스레드의 상태

스레드의 라이프 사이클

실행되기 전 상태
New Thread
실행 가능 상태
Runnable
실행을 끝낸 상태
Dead Thread
    ↑↓    
    실행 불가능 상태
Not Runnable
   

 

스레드의 상태를 알아내는 메소드

Thread.State state = thread.getState();
Thread.State 라는 타입은 Thread 클래스 안에 선언되어 있는 State 열거 타입

 

열거 타입 Thread.State의 열거값

열거 상수 의미하는 스레드의 상태
NEW 실행되기 전 상태
RUNNABLE 실행 가능 상태
WAITING wait 메소드를 호출하고 있는 상태
TIMED_WAITING sleep 메소드를 호출하고 있는 상태
BLOCKED 다른 스레드의 동기화 블록이나 동기화 메소드가 끝나기를 기다리고 있는 상태
TERMINATED 실행을 마친 상태

 

라이프 사이클에 해당하는 열거값

  • New Thread
    • NEW
  • Runnable
    • RUNNABLE
  • Dead Thread
    • TERMINATED
  • Not Runnable
    • WAITING
    • TIMED_WAITING
    • BLOCKED

사용 방법

class MonitorThread extends Thread {
    Thread thread;
    MonitorThread (Thread thread) {
        this.thread = thread;
    }
    public void run() {
        while (true) {
            Thread.State state = thread.getState();
            System.out.println("스레드의 상태: " + state);
            if (state == Thread.State.TERMINATED) {
                break;
            }
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
class Main {
    public static void main(String[] args) {
        CalcThread thread1 = new CalcThread();
        PrintThread thread2 = new PrintThread();
        MonitorThread thread3 = new MonitorThread(thread1); //thread1 모니터링 스레드 생성
        SharedArea obj = new SharedArea();
        thread1.sharedArea = obj;
        thread2.sharedArea = obj;
        thread1.start();
        thread2.start();
        thread3.start();  // 모니터링 스레드 시작
    }
}

NEW 상태가 안 보이는 이유는 모니터링 스레드가 실행되기 전에 지나가 버렸기 때문이다.

 

참고

<뇌를 자극하는 Java 프로그래밍>, 한빛미디어

'Language > Java' 카테고리의 다른 글

Exception 클래스  (0) 2022.10.18
우아한테크캠프 Pro 5기 프리코스 - JUnit, AssertJ 학습하기  (2) 2022.10.04
자바 자료구조  (0) 2022.07.04
Overflow가 발생하면 어떻게 될까?  (0) 2022.04.27
JVM의 Garbage Collector  (0) 2021.04.09
댓글
Total
Today
Yesterday
링크
Apple 2023 맥북 프로 14 M3, 스페이스 그레이, M3 8코어, 10코어 GPU, 512GB, 8GB, 한글