본문 바로가기
JAVA

멀티 스레드

by 융디's 2024. 4. 27.
728x90
멀티 스레드

멀티 스레드

@2024.04.18

프로세스와 멀티 스레드

프로세스

💡
프로세스실행 중인 프로그램의 인스턴스를 뜻하며,
운영체제는 각 프로세스에 메모리와 시스템 리소스를 할당한다.
  • 독립적인 메모리 공간(코드, 데이터, 힙, 스택 등)을 가지며, 다른 프로세스와는 격리되어있다.
    • 프로세스 간의 안정성 보장
    • 리소스 관리 측면에서는 비효율적이며, 프로세스 간 통신이 복잡해진다.

멀티 스레드

💡
하나의 프로세스 내에서 여러 개의 스레드를 동시에 실행하는 기법
  • 스레드 : 프로세스 내에서 실제로 작업을 수행하는 최소 단위
    • 프로세스의 리소스를 공유한다.
    • 각 스레드는 독립적인 실행 흐름을 가지며, 자신만의 스택을 가지나, 힙 메모리와 코드 영역은 공유
  • 장점
    • 성능 향상
      • 여러 작업을 동시에 처리할 수 있기에 응용 프로그램 처리 성능이 향상
      • ex) 채팅 프로그램 → 클라이언트의 입력을 받으면서, 서버로부터 메시지를 받는다.
    • 자원 효율성
      • 프로세스보다 스레드를 생성하고 관리하는 비용이 낮다.
    • 응답성 향상
      • 사용자 인터페이스와 같이 빠른 응답이 필요한 애플리케이션에서는 멀티 스레드 사용 시 사용자 경험을 개선할 수 있다.
  • 단점
    • 동시성 문제
      • 여러 스레드가 동일한 자원에 접근할 때 발생 → 동기화가 필요
    • 복잡성
      • 스레드 간의 상호작용과 동시성 관리는 프로그램의 복잡성을 증가시킨다.
    • 디버깅의 어려움
      • 디버깅이 어렵고, 잠재적인 버그의 원인을 찾기가 더 어렵다.

프로세스와 멀티 스레드의 차이점

  • 메모리 공유
    • 프로세스 : 완전히 독립적인 메모리 공간
    • 멀티 스레드 : 여러 스레드가 같은 메모리 공간(heap)을 공유
  • 통신
    • 프로세스 : 프로세스 간 통신을 하려면 IPC라는 방법을 사용하므로 어렵다.
    • 멀티 스레드 : 같은 프로세스의 메모리를 공유하기에 데이터 공유가 쉽다
  • 오버헤드
    • 어떤 작업을 수행하기 위해 추가로 필요한 리소스/시간
      • 멀티 스레드 vs 프로세스에서의 "오버헤드"는 추가적으로 발생하는 비용을 의미
    • 프로세스 : 더 많은 시스템 리소스를 필요로 한다.
    • 멀티 스레드 : 프로세스보다 생성과 관리 측면에서 오버헤드가 적다.
  • 안정성
    • 프로세스 : 서로 독립적이라 안정성이 높다
    • 멀티 스레드 : 한 스레드의 오류가 전체 프로세스에 영향을 줄 수 있다.

멀티 스레드 생성과 실행

Thread 클래스 상속 받기

class MYThread extends Thread{
	@Override
	public void run(){
		// 스레드가 실행 할 작업
	}
}

public static void main(String[] args){
	MYThread t = new MyThread();
	t.start(); // 스레드 실행
  • 장점
    • 구현이 간단하고 직관적
    • Thread 클래스의 다른 메서드들을 직접 사용 가능
  • 단점
    • 자바는 다중 상속이 지원하지 않으므로, Thread를 상속받으면 다른 클래스를 상속받을 수 없다

Runnable Interface 구현하기

class MyRunnable implments Runnable{
	@Override
	public void run(){
		// 스레드가 실행 할 작업
	}
}

public static void main(String[] args){
	Thread thread = new Thread(new MyRunnable());
}
  • 장점
    • 클래스가 다른 클래스를 상속받을 수 있으므로, 유연성이 높아진다.
  • 단점
    • Runnable 인터페이스에는 Thread 클래스의 다른 유용한 메서드들이 포함되어 있지 않다.
    • 명시적으로 Thread 객체를 생성해야 한다.

멀티 스레드 제어

  • 멀티 스레드 제어의 필요성
    • 데이터 일관성과 안정성 유지
      • 여러 스레드가 동일한 데이터를 접근/변경 → 데이터 일관성과 안정성 손상
      • 동기화를 사용하여 관리해야 한다.
    • 데드락 방지
      • 여러 스레드가 서로 소유한 리소스의 해제를 기다리며 무한 대기하는 것
      • 스레드 간 리소스 할당과 해제를 적절히 관리해야 한다.
    • 자원 공유 최적화
      • 여러 스레드가 시스템 자원을 공유하기에, 스레드 수가 많아지면 자원 경쟁이 심화
      • 자원을 효율적으로 공유하고 관리하는 전략 필요
    • 스레드 생명 주기 관리
      • 사용하지 않는 스레드는 자원 낭비한다.
      • 필요에 따라 스레드를 중지하거나, 재활용
    • 성능 최적화 및 부화 관리
      • 멀티 스레드는 시스템의 멀티 코어 프로세서를 효율적으로 활용한다.
      • 스레드를 너무 많이 생성 시, 컨텍스트 스위칭으로 인해 오버헤드가 증가
      • 적절한 스레드 수와 스레드 풀 관리는 필수다.

동기화(Synchronization)

💡
멀티 스레드를 제어하기 위해 자바가 제공하는 기능 중 하나로,

한 시점에 하나의 스레드 만이 해당 코드 영역에 접근할 수 있도록 한다.
  • 공유 자원에 대한 동시 접근을 방지하고, 데이터의 일관성과 안정성을 보장
  • synchoronized 키워드 사용
  • 예시
     private Map<String,PrintWriter> chatClients;
     // 만약 여러 스레드에서 put 메소드를 사용한다면? 
     // 동기화 작업이 필요하다!
     
    synchronized (chatClients){
        chatClients.put(this.id,out);
    }

스레드 생명 주기

💡
프로그래밍에서 스레드가 생성되고 실행되며 종료될 때 거치는 여러 상태들의 시퀀스로

생성(Creation), 실행(Runnable), 대기(Blocked/Waiting/Timed Waiting), 종료(Terminated)

단계로 구성되어 있다.
  • 사용하지 않는 스레드는 자원을 낭비하므로, 필요에 따라 스레드를 중지하거나 재활용하는 등의 관리를 해주어야 한다!
  • 단계
    • 생성(Creation)
      • 스레드가 생성되는 단계
      • 스레드 객체가 생성되고 초기화한다. ← 실행 X
    • 실행(Runnable)
      • 생성된 스레드가 실행 가능한 상태에 들어가는 단계
      • 스레드 스케줄러에 의해 선택되어 CPU를 할당받을 수 있는 상태
    • 대기(Blocked/Waiting/Timed Waiting)
      • 스레드가 실행되는 필요조건이 충족되지 않아 일시 중단되는 상태
      • ex) 다른 스레드의 자원을 기다리거나, 입출력 연산을 기다리는 동안 스레드는 대기 상태
    • 종료(Terminated)
      • 스레드가 실행을 완료하거나 중단되었을 때 상태
      • 종료 시 해당 스레드의 수명이 끝나게 된다.
  • 메서드
    • start()
      • 스레드를 실행시키기 위해 호출되는 메서드
      • 메서드가 호출되면 스레드가 생성되고 run()가 실행된다.
    • run()
      • 스레드의 주된 작업을 정의하는 메서드
      • start()가 호출되면 이 메서드가 실행된다.
    • sleep(long millisseconds)
      • 현재 스레드를 일정 시간 동안 정지시키는 메서드
      • 일정 시간이 지나면 스레드는 다시 실행된다.
    • yield()
      • 다른 스레드에게 실행을 양보하고 현재 스레드를 실행 대기 상태로 만드는 메서드
    • join()
      • 다른 스레드가 종료될 때까지 현재 스레드를 대기시키는 메서드
      • ex) threadA.join() → threadA가 종료될 때까지 현재 스레드의 실행을 일시 정지
      class TaskThread extends Thread {
          private String taskName;
      
          public TaskThread(String taskName) {
              this.taskName = taskName;
          }
      		@Override
          public void run() {
              System.out.println(taskName + " 작업 시작");
              try {
                  Thread.sleep(2000);  // 2초 동안 스레드 일시 정지 (작업 시뮬레이션)
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              System.out.println(taskName + " 작업 완료");
          }
      }
      
      public class ThreadJoinExample {
      
          public static void main(String[] args) {
              TaskThread task1 = new TaskThread("작업 1");
              TaskThread task2 = new TaskThread("작업 2");
      
              DemonThread demonThread = new DemonThread();
      
              task1.start();
              task2.start();
      
              try {
                  System.out.println("모든 작업의 완료를 기다립니다.");
                  task1.join();  // task1의 완료를 기다림
                  task2.join();  // task2의 완료를 기다림
              } catch (Exception e) {
                  e.printStackTrace();
              }
      
              System.out.println("모든 작업이 완료되었습니다.");
          }
      }
    • SetPriority(), getPriority()
      • 스레드의 우선순위를 설정하거나 가져오는 메서드
    • interrupt()
      • 스레드를 중단시키는 요청을 하며, 스레드에 인터럽트 플래그를 설정하는 메서드
      • InterruptedExcption 예외를 발생시킨다.
        • isInterrupted(), interttupted() → 스레드가 인터럽트 되었는지를 여부를 확인
      public class ThreadInterruptExample {
          static class MyThread extends Thread {
              public void run() {
                  try {
                      for (int i = 0; i < 5; i++) {
                          Thread.sleep(1000);  // 1초 동안 일시 정지
                          System.out.println("Processing step " + (i + 1));
                      }
                  } catch (InterruptedException e) {
                      System.out.println("스레드가 중단되었습니다.");
                      return;  // 스레드를 안전하게 종료
                  }
              }
          }
      
          public static void main(String[] args) {
              MyThread t = new MyThread();
              t.start();  // 스레드 시작
      
              try {
                  Thread.sleep(2500);  // 메인 스레드를 2.5초 동안 일시 정지
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
      
              t.interrupt();  // 스레드에 인터럽트 신호 보내기
          }
      }
    • isAlive()
      • 스레드가 아직 실행 중 인지를 여부를 확인하는 메서드
    • wait()
      • 스레드를 대기 상태로 만드는 메서드
    • notify()
      • 대기 중인 스레드를 깨우는 메서드
      public class WaitNotifyExample {
          private static final Object lock = new Object();
          private static boolean itemAvailable = false;
      
          static class Producer extends Thread {
              public void run() {
                  synchronized (lock) {
                      System.out.println("생산자가 아이템을 생산 중입니다.");
                      itemAvailable = true;
                      lock.notify();  // 생산이 끝났으므로 소비자에게 알림
                      System.out.println("생산자가 알림을 보냈습니다.");
                  }
              }
          }
      
          static class Consumer extends Thread {
              public void run() {
                  synchronized (lock) { // lock의 소유권을 가진다.
                      while (!itemAvailable) {
                          try {
                              System.out.println("소비자가 아이템을 기다리고 있습니다.");
                              lock.wait();  // wait하면 lock의 소유권을 포기한다.
                          } catch (InterruptedException e) {
                              e.printStackTrace();
                          }
                      }
                      System.out.println("소비자가 아이템을 소비했습니다.");
                      itemAvailable = false; // 아이템 소비 후 상태 업데이트
                  }
              }
          }
      
          public static void main(String[] args) {
              Producer producer = new Producer();
              Consumer consumer = new Consumer();
      
              consumer.start(); // 소비자 스레드 시작
              try {
                  Thread.sleep(1000); // 생산자 시작 전에 잠시 대기
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              producer.start(); // 생산자 스레드 시작
          }
      }
    • notifyAll()
      • 대기 중인 모든 스레드를 깨우는 메서드
      public class WaitNotifyAllExample2 {
          private static final Object lock = new Object();
          private static int itemsAvailable = 0;  // 사용 가능한 아이템 수
      
          static class Producer extends Thread {
              public void run() {
                  synchronized (lock) {
                      itemsAvailable += 5;  // 5개의 아이템을 생산
                      System.out.println("생산자가 " + itemsAvailable + "개의 아이템을 생산하였습니다.");
                      lock.notifyAll();  // 모든 대기 중인 소비자 스레드에 알림
                      System.out.println("생산자가 모든 소비자에게 알림을 보냈습니다.");
                  }
              }
          }
      
          static class Consumer extends Thread {
              private int id;
      
              Consumer(int id) {
                  this.id = id;
              }
      
              public void run() {
                  synchronized (lock) {
                      while (itemsAvailable <= 0) {
                          try {
                              System.out.println("소비자 " + id + "가 아이템을 기다리고 있습니다.");
                              lock.wait();  // 아이템을 기다림
                          } catch (InterruptedException e) {
                              e.printStackTrace();
                          }
                      }
                      itemsAvailable--;  // 아이템 소비
                      System.out.println("소비자 " + id + "가 아이템을 소비했습니다. 남은 아이템: " + itemsAvailable);
                  }
              }
          }
      
          public static void main(String[] args) {
              Producer producer = new Producer();
              Consumer consumer1 = new Consumer(1);
              Consumer consumer2 = new Consumer(2);
              Consumer consumer3 = new Consumer(3);
      
              consumer1.start(); // 소비자 1 스레드 시작
              consumer2.start(); // 소비자 2 스레드 시작
              consumer3.start(); // 소비자 3 스레드 시작
      
              try {
                  Thread.sleep(1000); // 생산자 시작 전에 잠시 대기
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              producer.start(); // 생산자 스레드 시작
          }
      }

Demon Thread

💡
백 그라운드에서 특정 작업을 수행하는 스레드로,
일반적으로 다른 스레드들이 종속되지 않은
보조적인 작업을 한다
(가비지 컬렉션, 자동 저장, 데몬 서비스의 백그라운드 작업)
  • 자바 프로그램이 실행되는 동안 백그라운드에서 계속 실행되며, 데몬 스레드가 남아 있는 한
    자바 프로그램은 종료되지 않는다.
    ⇒ 단!
    모든 일반 스레드가 종료되면 데몬 스레드는 강제 종료
  • 주 스레드나 다른 일반 스레드들에 의해서 생성되며, 데몬 스레드는 해당 스레드들이 종료될 때 함께 종료
  • 예시
    • 무한 루프로 인해 모든 스레드가 종료되어도 demonThread는 계속 출력
    • demonThread.setDaemon(true)으로 데몬 스레드를 생성하면, task1과 task2가 종료되면
      demonThread도 종료된다.
      // 스레드 생성
      class DemonThread extends Thread {
          @Override
          public void run() {
              while (true) {
                  System.out.println("배경음악 재생중!!!");
                  try {
                      sleep(1000);
                  } catch (InterruptedException e) {
                      throw new RuntimeException(e);
                  }
              }
          }
      }
      
      public class ThreadJoinExample {
          static class TaskThread extends Thread {
              private String taskName;
      
              public TaskThread(String taskName) {
                  this.taskName = taskName;
              }
      
              public void run() {
                  System.out.println(taskName + " 작업 시작");
                  try {
                      Thread.sleep(2000);  // 2초 동안 스레드 일시 정지 (작업 시뮬레이션)
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  System.out.println(taskName + " 작업 완료");
              }
          }
      
          public static void main(String[] args) {
              TaskThread task1 = new TaskThread("작업 1");
              TaskThread task2 = new TaskThread("작업 2");
      
              DemonThread demonThread = new DemonThread();
      
              task1.start();
              task2.start();
      
              demonThread.setDaemon(true);
              demonThread.start();
      
              try {
                  System.out.println("모든 작업의 완료를 기다립니다.");
                  task1.join();  // task1의 완료를 기다림
                  task2.join();  // task2의 완료를 기다림
              } catch (Exception e) {
                  e.printStackTrace();
              }
      
              System.out.println("모든 작업이 완료되었습니다.");
          }
      }

728x90

'JAVA' 카테고리의 다른 글

TCP 프로그래밍  (1) 2024.04.27
네트워크의 기본  (0) 2024.04.27
java.io 패키지  (0) 2024.04.27
데코레이터 패턴  (1) 2024.04.27
컬렉션 프레임워크  (1) 2024.04.27